diff --git a/src/Concerns/CanMapDynamicFields.php b/src/Concerns/CanMapDynamicFields.php index cf6eb8e..62520b6 100644 --- a/src/Concerns/CanMapDynamicFields.php +++ b/src/Concerns/CanMapDynamicFields.php @@ -3,12 +3,12 @@ namespace Backstage\Fields\Concerns; use Backstage\Fields\Contracts\FieldInspector; -use Backstage\Fields\Enums\Field; use Backstage\Fields\Fields; use Backstage\Fields\Fields\Checkbox; use Backstage\Fields\Fields\CheckboxList; use Backstage\Fields\Fields\Color; use Backstage\Fields\Fields\DateTime; +use Backstage\Fields\Fields\FileUpload; use Backstage\Fields\Fields\KeyValue; use Backstage\Fields\Fields\MarkdownEditor; use Backstage\Fields\Fields\Radio; @@ -47,6 +47,7 @@ trait CanMapDynamicFields 'select' => Select::class, 'checkbox' => Checkbox::class, 'checkbox-list' => CheckboxList::class, + 'file-upload' => FileUpload::class, 'key-value' => KeyValue::class, 'radio' => Radio::class, 'toggle' => Toggle::class, @@ -317,6 +318,10 @@ protected function getFieldsFromBlocks(array $blocks): Collection collect($blocks)->map(function ($block) use (&$processedFields) { foreach ($block as $key => $values) { + if (! is_array($values) || ! isset($values['data'])) { + continue; + } + $fields = $values['data']; $fields = ModelsField::whereIn('ulid', array_keys($fields))->get(); diff --git a/src/Enums/Field.php b/src/Enums/Field.php index 6decf54..debd3b4 100644 --- a/src/Enums/Field.php +++ b/src/Enums/Field.php @@ -12,7 +12,7 @@ enum Field: string case CheckboxList = 'checkbox-list'; case Color = 'color'; case DateTime = 'date-time'; - // case File = 'file-upload'; + case File = 'file-upload'; // case Hidden = 'hidden'; case KeyValue = 'key-value'; // case Link = 'link'; diff --git a/src/Fields/FileUpload.php b/src/Fields/FileUpload.php new file mode 100644 index 0000000..2320c36 --- /dev/null +++ b/src/Fields/FileUpload.php @@ -0,0 +1,231 @@ + 'public', + 'directory' => 'uploads', + 'visibility' => 'public', + 'acceptedFileTypes' => null, + 'maxSize' => null, + 'maxFiles' => 1, + 'multiple' => false, + 'appendFiles' => false, + 'reorderable' => false, + 'openable' => true, + 'downloadable' => true, + 'previewable' => true, + 'deletable' => true, + ]; + } + + public static function make(string $name, ?Field $field = null): FilamentFileUpload + { + $config = array_merge(self::getDefaultConfig(), $field->config ?? []); + + $component = FilamentFileUpload::make($name) + ->label($field->name ?? null) + ->disk($config['disk']) + ->directory($config['directory']) + ->visibility($config['visibility']) + ->maxFiles($config['maxFiles']) + ->multiple($config['multiple']) + ->appendFiles($config['appendFiles']) + ->reorderable($config['reorderable']) + ->openable($config['openable']) + ->downloadable($config['downloadable']) + ->previewable($config['previewable']) + ->deletable($config['deletable']); + + if ($config['acceptedFileTypes']) { + $component->acceptedFileTypes(explode(',', $config['acceptedFileTypes'])); + } + + if ($config['maxSize']) { + $component->maxSize($config['maxSize']); + } + + return self::applyDefaultSettings($component, $field); + } + + public static function mutateFormDataCallback(Model $record, Field $field, array $data): array + { + if (! property_exists($record, 'valueColumn') || ! isset($record->values[$field->ulid])) { + return $data; + } + + $data[$record->valueColumn][$field->ulid] = self::decodeFileValueForForm($record->values[$field->ulid]); + + return $data; + } + + public static function mutateBeforeSaveCallback(Model $record, Field $field, array $data): array + { + if (! property_exists($record, 'valueColumn') || ! isset($data[$record->valueColumn][$field->ulid])) { + return $data; + } + + $data[$record->valueColumn][$field->ulid] = self::normalizeFileValue($data[$record->valueColumn][$field->ulid]); + + return $data; + } + + private static function decodeFileValueForForm(mixed $value): array + { + if (is_null($value) || $value === '') { + return []; + } + + if (is_array($value)) { + return $value; + } + + if (is_string($value) && json_validate($value)) { + $decoded = json_decode($value, true); + + return is_array($decoded) ? $decoded : []; + } + + if (is_string($value) && ! empty($value)) { + return [$value]; + } + + if (! empty($value)) { + return [(string) $value]; + } + + return []; + } + + private static function normalizeFileValue(mixed $value): ?string + { + if (is_null($value) || $value === '') { + return null; + } + + if (is_array($value)) { + return json_encode($value); + } + + if (is_string($value) && json_validate($value)) { + return $value; + } + + if (is_string($value) && ! empty($value)) { + return json_encode([$value]); + } + + if (! empty($value)) { + return json_encode([(string) $value]); + } + + return null; + } + + public function getForm(): array + { + return [ + Tabs::make() + ->schema([ + Tab::make('General') + ->label(__('General')) + ->schema([ + ...parent::getForm(), + ]), + Tab::make('Field specific') + ->label(__('Field specific')) + ->schema([ + Grid::make(2) + ->schema([ + TextInput::make('config.disk') + ->label(__('Storage Disk')) + ->default('public') + ->required(), + + TextInput::make('config.directory') + ->label(__('Upload Directory')) + ->default('uploads') + ->required(), + + Select::make('config.visibility') + ->label(__('File Visibility')) + ->options([ + 'public' => __('Public'), + 'private' => __('Private'), + ]) + ->default('public') + ->required(), + + TextInput::make('config.acceptedFileTypes') + ->label(__('Accepted File Types')) + ->placeholder('image/*,application/pdf') + ->helperText(__('Comma-separated list of MIME types or file extensions')), + + TextInput::make('config.maxSize') + ->label(__('Max File Size (KB)')) + ->numeric() + ->minValue(1), + + TextInput::make('config.maxFiles') + ->label(__('Max Files')) + ->numeric() + ->minValue(1) + ->default(1) + ->required(), + ]), + + Grid::make(2) + ->schema([ + Toggle::make('config.multiple') + ->label(__('Multiple Files')) + ->helperText(__('Allow multiple file selection')) + ->live(), + + Toggle::make('config.appendFiles') + ->label(__('Append Files')) + ->helperText(__('Append new files to existing ones')) + ->visible(fn (Get $get): bool => $get('config.multiple')), + + Toggle::make('config.reorderable') + ->label(__('Reorderable')) + ->helperText(__('Allow reordering of files')) + ->visible(fn (Get $get): bool => $get('config.multiple')), + + Toggle::make('config.openable') + ->label(__('Openable')) + ->helperText(__('Allow opening files in new tab')), + + Toggle::make('config.downloadable') + ->label(__('Downloadable')) + ->helperText(__('Allow downloading files')), + + Toggle::make('config.previewable') + ->label(__('Previewable')) + ->helperText(__('Allow previewing files')), + + Toggle::make('config.deletable') + ->label(__('Deletable')) + ->helperText(__('Allow deleting files')), + ]), + ]), + ])->columnSpanFull(), + ]; + } +}