From 8be9b04814127c93cf19b7d15a714c28d64f49fd Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 21:00:33 +0200 Subject: [PATCH 01/11] fix: option type when creating new records --- src/Concerns/HasDatalist.php | 2 +- src/Concerns/HasOptions.php | 2 +- src/Concerns/HasSelectableValues.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) 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..ae04408 100644 --- a/src/Concerns/HasSelectableValues.php +++ b/src/Concerns/HasSelectableValues.php @@ -145,9 +145,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(), From 3cbc673069a3876680c935df52991ee2025b59f4 Mon Sep 17 00:00:00 2001 From: Baspa <10845460+Baspa@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:00:53 +0000 Subject: [PATCH 02/11] Fix styling --- src/Concerns/HasSelectableValues.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/HasSelectableValues.php b/src/Concerns/HasSelectableValues.php index ae04408..af52740 100644 --- a/src/Concerns/HasSelectableValues.php +++ b/src/Concerns/HasSelectableValues.php @@ -145,7 +145,7 @@ protected function selectableValuesFormFields(string $type, string $label, strin ]) ->afterStateHydrated(function (Forms\Get $get, Forms\Set $set) use ($type) { $value = $get("config.{$type}"); - + // Set correct config value when creating records $set("config.{$type}", is_array($value) ? $value : (is_bool($value) ? [] : [$value])); }) From 740bac02ca280f57f2bf1646f7736fb1dfac2aa7 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 21:42:08 +0200 Subject: [PATCH 03/11] fix: field config error for datalistType --- src/Concerns/HasSelectableValues.php | 174 ++++++++++++++++++--------- 1 file changed, 118 insertions(+), 56 deletions(-) diff --git a/src/Concerns/HasSelectableValues.php b/src/Concerns/HasSelectableValues.php index ae04408..5709ed4 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; + } + + 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])); + } - if ($results->isEmpty()) { - continue; - } + 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; + } + + 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])); + } - $opts = $results->pluck($relation['relationValue'] ?? 'name', $relation['relationKey'])->toArray(); + protected static function buildRelationshipOptions(mixed $field): array + { + $relationshipOptions = []; - if (count($opts) === 0) { - continue; - } + foreach ($field->config['relations'] ?? [] as $relation) { + if (!isset($relation['resource'])) { + continue; + } + + $model = static::resolveResourceModel($relation['resource']); - // Group by resource name - $resourceName = Str::title($relation['resource']); - $relationshipOptions[$resourceName] = $opts; + 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 From f272692439f050df3e1c8df010e63ec0b94e4e5f Mon Sep 17 00:00:00 2001 From: Baspa <10845460+Baspa@users.noreply.github.com> Date: Fri, 4 Jul 2025 19:42:42 +0000 Subject: [PATCH 04/11] Fix styling --- src/Concerns/HasSelectableValues.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Concerns/HasSelectableValues.php b/src/Concerns/HasSelectableValues.php index 0a042d1..38a450e 100644 --- a/src/Concerns/HasSelectableValues.php +++ b/src/Concerns/HasSelectableValues.php @@ -35,7 +35,7 @@ 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)) { + if (! static::ensureFieldConfig($field, $type)) { return $input; } @@ -53,7 +53,7 @@ protected static function addValuesToInput(mixed $input, mixed $field, string $t } // Apply all merged options to the input - if (!empty($allOptions)) { + if (! empty($allOptions)) { $input->$method($allOptions); } @@ -63,12 +63,12 @@ protected static function addValuesToInput(mixed $input, mixed $field, string $t 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)) { + if (! isset($field->config) || ! is_array($field->config)) { return false; } // Ensure the type key exists in the config to prevent undefined array key errors - if (!array_key_exists($type, $field->config)) { + if (! array_key_exists($type, $field->config)) { $config = $field->config ?? []; $config[$type] = null; $field->config = $config; @@ -80,10 +80,10 @@ protected static function ensureFieldConfig(mixed $field, string $type): bool protected static function shouldHandleRelationshipOptions(mixed $field, string $type): bool { // Ensure $type is a string to prevent array key errors - if (!is_string($type)) { + if (! is_string($type)) { return false; } - + 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])); @@ -92,10 +92,10 @@ protected static function shouldHandleRelationshipOptions(mixed $field, string $ protected static function shouldHandleArrayOptions(mixed $field, string $type): bool { // Ensure $type is a string to prevent array key errors - if (!is_string($type)) { + if (! is_string($type)) { return false; } - + 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])); @@ -106,13 +106,13 @@ protected static function buildRelationshipOptions(mixed $field): array $relationshipOptions = []; foreach ($field->config['relations'] ?? [] as $relation) { - if (!isset($relation['resource'])) { + if (! isset($relation['resource'])) { continue; } $model = static::resolveResourceModel($relation['resource']); - if (!$model) { + if (! $model) { continue; } @@ -165,7 +165,7 @@ protected static function mergeRelationshipOptions(array $allOptions, array $rel protected static function mergeArrayOptions(array $allOptions, mixed $field, string $type): array { - if (!isset($field->config['options']) || !is_array($field->config['options'])) { + if (! isset($field->config['options']) || ! is_array($field->config['options'])) { return $allOptions; } From c7a1a5d3510f37a46cd34f725a0103159f477fef Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 21:58:26 +0200 Subject: [PATCH 05/11] improve form data mutations to support switching multiple/single options --- src/Fields/Select.php | 56 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Fields/Select.php b/src/Fields/Select.php index 39bb65e..489d9b4 100644 --- a/src/Fields/Select.php +++ b/src/Fields/Select.php @@ -74,6 +74,62 @@ public static function make(string $name, ?Field $field = null): Input return $input; } + public static function mutateFormDataCallback($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($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 [ From cef829e01b70206873e1e3f6e3d5b582276d9d58 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 22:06:14 +0200 Subject: [PATCH 06/11] add helper text to warn users --- src/Fields/Select.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Fields/Select.php b/src/Fields/Select.php index 489d9b4..70628c5 100644 --- a/src/Fields/Select.php +++ b/src/Fields/Select.php @@ -151,6 +151,8 @@ public function getForm(): array ->inline(false), Forms\Components\Toggle::make('config.multiple') ->label(__('Multiple')) + ->helperText(__('When switching from multiple to single, the first value from existing values will be used.')) + ->columnSpan(2) ->inline(false), Forms\Components\Toggle::make('config.allowHtml') ->label(__('Allow HTML')) From abe8d3f4e7fe3c13ad8c2ee5a8458590344b4de4 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 22:06:24 +0200 Subject: [PATCH 07/11] clean up and explain code --- src/Concerns/CanMapDynamicFields.php | 383 ++++++++++++++++++++++++--- 1 file changed, 352 insertions(+), 31 deletions(-) diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index d40e915..bc7260c 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -23,10 +23,24 @@ 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; + /** + * Maps field type strings to their corresponding field class implementations. + * Used as a fallback when custom fields are not available. + */ private const FIELD_TYPE_MAP = [ 'text' => Text::class, 'textarea' => Textarea::class, @@ -43,66 +57,296 @@ trait CanMapDynamicFields 'tags' => Tags::class, ]; + /** + * Initialize the field inspector service. + * Called during the component's boot process. + */ public function boot(): void { $this->fieldInspector = app(FieldInspector::class); } + /** + * Handle field refresh events from Livewire. + * Currently a placeholder for future implementation. + */ #[On('refreshFields')] 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); + }); + } + + /** + * Check if the current record exists and has fields. + * + * @return bool True if record exists and has fields + */ + private function hasValidRecordWithFields(): bool + { + return isset($this->record) && ! $this->record->fields->isEmpty(); + } + + /** + * Check if the current record exists. + * + * @return bool True if record exists + */ + private function hasValidRecord(): bool + { + return isset($this->record); + } - $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 form values from the data array. + * + * @param array $data The form data + * @return array The extracted values + */ + private function extractFormValues(array $data): array + { + return isset($data[$this->record?->valueColumn]) ? $data[$this->record?->valueColumn] : []; + } + + /** + * 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'])) { + 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; + } + /** + * 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; + } + + /** + * 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 +360,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 +385,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 +404,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; @@ -168,28 +459,58 @@ private function resolveFormFields(mixed $record = null, bool $isNested = false) ->all(); } + /** + * Resolve custom field implementations. + * + * @return Collection Collection of custom field instances + */ private function resolveCustomFields(): Collection { return collect(Fields::getFields()) ->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; } + + /** + * Generate the input name for a field. + * + * @param Model $field The field model + * @param mixed $record The record + * @param bool $isNested Whether this is a nested field + * @return string The input name + */ + private function generateInputName(Model $field, mixed $record, bool $isNested): string + { + return $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}"; + } } From db051331823cf7918b4466f96024d5921b0d30f6 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 22:08:27 +0200 Subject: [PATCH 08/11] add nested fields and builder support --- src/Concerns/CanMapDynamicFields.php | 163 ++++++++++++++++++--------- 1 file changed, 109 insertions(+), 54 deletions(-) diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index bc7260c..b6eb279 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -207,6 +207,12 @@ private function getAllFieldsIncludingBuilderFields(array $builderBlocks): Colle 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 $fieldInstance->mutateFormDataCallback($this->record, $field, $data); } @@ -217,67 +223,28 @@ private function applyFieldFillMutation(Model $field, array $fieldConfig, object } /** - * 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. + * Extract builder blocks from record values. * - * @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 + * @return array The builder blocks */ - private function applyFieldSaveMutation(Model $field, array $fieldConfig, object $fieldInstance, array $data, array $builderBlocks): array + private function extractBuilderBlocksFromRecord(): 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); + if (!isset($this->record->values) || !is_array($this->record->values)) { + return []; } - // 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'], - ]; - } - } - } - } + $builderFieldUlids = ModelsField::whereIn('ulid', array_keys($this->record->values)) + ->where('field_type', 'builder') + ->pluck('ulid') + ->toArray(); - return [ - 'isInBuilder' => false, - 'builderData' => null, - ]; + return collect($this->record->values) + ->filter(fn ($value, $key) => in_array($key, $builderFieldUlids)) + ->toArray(); } /** - * 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. + * Process fill mutation for fields inside builder blocks. * * @param Model $field The field model * @param object $fieldInstance The field instance @@ -286,14 +253,14 @@ private function determineFieldLocation(Model $field, array $builderBlocks): arr * @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 + 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->mutateBeforeSaveCallback($mockRecord, $field, $tempData); + $tempData = $fieldInstance->mutateFormDataCallback($mockRecord, $field, $tempData); // Update the original data structure with the mutated values $this->updateBuilderBlocksWithMutatedData($builderBlocks, $field, $tempData); @@ -513,4 +480,92 @@ private function generateInputName(Model $field, mixed $record, bool $isNested): { 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; + } } From 3a2cbf9a4a1c5d098b08eb299469597ab671e4c1 Mon Sep 17 00:00:00 2001 From: Baspa <10845460+Baspa@users.noreply.github.com> Date: Fri, 4 Jul 2025 20:08:53 +0000 Subject: [PATCH 09/11] Fix styling --- src/Concerns/CanMapDynamicFields.php | 184 +++++++++++++-------------- src/Fields/Select.php | 7 +- 2 files changed, 95 insertions(+), 96 deletions(-) diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index b6eb279..77fd429 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -25,7 +25,7 @@ /** * 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) @@ -78,11 +78,11 @@ 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 + * + * @param array $data The form data array * @return array The mutated form data */ protected function mutateBeforeFill(array $data): array @@ -102,12 +102,12 @@ protected function mutateBeforeFill(array $data): array /** * 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 + * + * @param array $data The form data array * @return array The mutated form data ready for saving */ protected function mutateBeforeSave(array $data): array @@ -131,7 +131,7 @@ protected function mutateBeforeSave(array $data): array /** * Check if the current record exists and has fields. - * + * * @return bool True if record exists and has fields */ private function hasValidRecordWithFields(): bool @@ -141,7 +141,7 @@ private function hasValidRecordWithFields(): bool /** * Check if the current record exists. - * + * * @return bool True if record exists */ private function hasValidRecord(): bool @@ -151,8 +151,8 @@ private function hasValidRecord(): bool /** * Extract form values from the data array. - * - * @param array $data The form data + * + * @param array $data The form data * @return array The extracted values */ private function extractFormValues(array $data): array @@ -162,11 +162,11 @@ private function extractFormValues(array $data): array /** * 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 + * + * @param array $values The form values * @return array The builder blocks */ private function extractBuilderBlocks(array $values): array @@ -183,8 +183,8 @@ private function extractBuilderBlocks(array $values): array /** * Get all fields including those from builder blocks. - * - * @param array $builderBlocks The builder blocks + * + * @param array $builderBlocks The builder blocks * @return Collection All fields to process */ private function getAllFieldsIncludingBuilderFields(array $builderBlocks): Collection @@ -196,23 +196,23 @@ private function getAllFieldsIncludingBuilderFields(array $builderBlocks): Colle /** * 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 + * + * @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 $fieldInstance->mutateFormDataCallback($this->record, $field, $data); } @@ -224,12 +224,12 @@ private function applyFieldFillMutation(Model $field, array $fieldConfig, object /** * 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)) { + if (! isset($this->record->values) || ! is_array($this->record->values)) { return []; } @@ -245,26 +245,26 @@ private function extractBuilderBlocksFromRecord(): array /** * 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 + * + * @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); @@ -273,24 +273,24 @@ private function processBuilderFieldFillMutation(Model $field, object $fieldInst /** * Create a mock record for builder field processing. - * - * @param array $builderData The builder block data + * + * @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 + * + * @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 { @@ -307,11 +307,11 @@ private function updateBuilderBlocksWithMutatedData(array &$builderBlocks, Model /** * 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 + * + * @param Model $field The field model * @return array Array containing 'config' and 'instance' keys */ private function resolveFieldConfigAndInstance(Model $field): array @@ -329,11 +329,11 @@ 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 + * + * @param array $blocks The builder blocks * @return Collection The field models from blocks */ protected function getFieldsFromBlocks(array $blocks): Collection @@ -354,13 +354,13 @@ protected function getFieldsFromBlocks(array $blocks): Collection /** * 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 + * + * @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 @@ -379,10 +379,10 @@ protected function mutateFormData(array $data, Collection $fields, callable $mut /** * 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 + * + * @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 @@ -401,12 +401,12 @@ private function processNestedFields(Model $field, array $data, callable $mutati /** * 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 + * + * @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 @@ -428,7 +428,7 @@ private function resolveFormFields(mixed $record = null, bool $isNested = false) /** * Resolve custom field implementations. - * + * * @return Collection Collection of custom field instances */ private function resolveCustomFields(): Collection @@ -439,14 +439,14 @@ private function resolveCustomFields(): Collection /** * 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 + * + * @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 @@ -470,10 +470,10 @@ private function resolveFieldInput(Model $field, Collection $customFields, mixed /** * Generate the input name for a field. - * - * @param Model $field The field model - * @param mixed $record The record - * @param bool $isNested Whether this is a nested field + * + * @param Model $field The field model + * @param mixed $record The record + * @param bool $isNested Whether this is a nested field * @return string The input name */ private function generateInputName(Model $field, mixed $record, bool $isNested): string @@ -483,15 +483,15 @@ private function generateInputName(Model $field, mixed $record, bool $isNested): /** * 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 + * + * @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 @@ -512,9 +512,9 @@ private function applyFieldSaveMutation(Model $field, array $fieldConfig, object /** * 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 + * + * @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 @@ -540,29 +540,29 @@ private function determineFieldLocation(Model $field, array $builderBlocks): arr /** * 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 + * + * @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); diff --git a/src/Fields/Select.php b/src/Fields/Select.php index 70628c5..cc711da 100644 --- a/src/Fields/Select.php +++ b/src/Fields/Select.php @@ -79,7 +79,6 @@ public static function mutateFormDataCallback($record, $field, array $data): arr 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); @@ -100,7 +99,7 @@ public static function mutateBeforeSaveCallback($record, $field, array $data): a } /** - * Normalize the select value to an array or a single value. This is needed because the select field can be + * 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 @@ -118,12 +117,12 @@ private static function normalizeSelectValue($value, Field $field): mixed } // Convert to array if multiple is expected but value is not an array - if ($isMultiple && !is_array($value)) { + if ($isMultiple && ! is_array($value)) { return [$value]; } // Convert array to single value if multiple is not expected - if (!$isMultiple && is_array($value)) { + if (! $isMultiple && is_array($value)) { return empty($value) ? null : reset($value); } From fb2e07424f1b3511932e807aba3339d59b7787c6 Mon Sep 17 00:00:00 2001 From: Baspa Date: Fri, 4 Jul 2025 22:16:01 +0200 Subject: [PATCH 10/11] remove some of the comments --- src/Concerns/CanMapDynamicFields.php | 41 ---------------------------- 1 file changed, 41 deletions(-) diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index b6eb279..33e768e 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -37,10 +37,6 @@ trait CanMapDynamicFields { private FieldInspector $fieldInspector; - /** - * Maps field type strings to their corresponding field class implementations. - * Used as a fallback when custom fields are not available. - */ private const FIELD_TYPE_MAP = [ 'text' => Text::class, 'textarea' => Textarea::class, @@ -57,19 +53,11 @@ trait CanMapDynamicFields 'tags' => Tags::class, ]; - /** - * Initialize the field inspector service. - * Called during the component's boot process. - */ public function boot(): void { $this->fieldInspector = app(FieldInspector::class); } - /** - * Handle field refresh events from Livewire. - * Currently a placeholder for future implementation. - */ #[On('refreshFields')] public function refresh(): void { @@ -129,32 +117,16 @@ protected function mutateBeforeSave(array $data): array }); } - /** - * Check if the current record exists and has fields. - * - * @return bool True if record exists and has fields - */ private function hasValidRecordWithFields(): bool { return isset($this->record) && ! $this->record->fields->isEmpty(); } - /** - * Check if the current record exists. - * - * @return bool True if record exists - */ private function hasValidRecord(): bool { return isset($this->record); } - /** - * Extract form values from the data array. - * - * @param array $data The form data - * @return array The extracted values - */ private function extractFormValues(array $data): array { return isset($data[$this->record?->valueColumn]) ? $data[$this->record?->valueColumn] : []; @@ -426,11 +398,6 @@ private function resolveFormFields(mixed $record = null, bool $isNested = false) ->all(); } - /** - * Resolve custom field implementations. - * - * @return Collection Collection of custom field instances - */ private function resolveCustomFields(): Collection { return collect(Fields::getFields()) @@ -468,14 +435,6 @@ private function resolveFieldInput(Model $field, Collection $customFields, mixed return null; } - /** - * Generate the input name for a field. - * - * @param Model $field The field model - * @param mixed $record The record - * @param bool $isNested Whether this is a nested field - * @return string The input name - */ private function generateInputName(Model $field, mixed $record, bool $isNested): string { return $isNested ? "{$field->ulid}" : "{$record->valueColumn}.{$field->ulid}"; From 19f25991c50834285de378b74bddea5968df937b Mon Sep 17 00:00:00 2001 From: Baspa Date: Sun, 6 Jul 2025 08:29:17 +0200 Subject: [PATCH 11/11] change text and correct type hints --- src/Fields/Select.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Fields/Select.php b/src/Fields/Select.php index cc711da..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,7 +75,7 @@ public static function make(string $name, ?Field $field = null): Input return $input; } - public static function mutateFormDataCallback($record, $field, array $data): array + public static function mutateFormDataCallback(Model $record, $field, array $data): array { if (! property_exists($record, 'valueColumn') || ! isset($record->values[$field->ulid])) { return $data; @@ -86,7 +87,7 @@ public static function mutateFormDataCallback($record, $field, array $data): arr return $data; } - public static function mutateBeforeSaveCallback($record, $field, array $data): array + public static function mutateBeforeSaveCallback(Model $record, $field, array $data): array { if (! property_exists($record, 'valueColumn') || ! isset($data[$record->valueColumn][$field->ulid])) { return $data; @@ -150,7 +151,7 @@ public function getForm(): array ->inline(false), Forms\Components\Toggle::make('config.multiple') ->label(__('Multiple')) - ->helperText(__('When switching from multiple to single, the first value from existing values will be used.')) + ->helperText(__('Only first value is used when switching from multiple to single.')) ->columnSpan(2) ->inline(false), Forms\Components\Toggle::make('config.allowHtml')