Skip to content

Commit bf115aa

Browse files
authored
feat: file uploader (#39)
* wip * fix: use ulids * feat: fix loading images * fix: phpstan issues
1 parent cdb3baf commit bf115aa

File tree

3 files changed

+238
-2
lines changed

3 files changed

+238
-2
lines changed

src/Concerns/CanMapDynamicFields.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
namespace Backstage\Fields\Concerns;
44

55
use Backstage\Fields\Contracts\FieldInspector;
6-
use Backstage\Fields\Enums\Field;
76
use Backstage\Fields\Fields;
87
use Backstage\Fields\Fields\Checkbox;
98
use Backstage\Fields\Fields\CheckboxList;
109
use Backstage\Fields\Fields\Color;
1110
use Backstage\Fields\Fields\DateTime;
11+
use Backstage\Fields\Fields\FileUpload;
1212
use Backstage\Fields\Fields\KeyValue;
1313
use Backstage\Fields\Fields\MarkdownEditor;
1414
use Backstage\Fields\Fields\Radio;
@@ -47,6 +47,7 @@ trait CanMapDynamicFields
4747
'select' => Select::class,
4848
'checkbox' => Checkbox::class,
4949
'checkbox-list' => CheckboxList::class,
50+
'file-upload' => FileUpload::class,
5051
'key-value' => KeyValue::class,
5152
'radio' => Radio::class,
5253
'toggle' => Toggle::class,
@@ -317,6 +318,10 @@ protected function getFieldsFromBlocks(array $blocks): Collection
317318

318319
collect($blocks)->map(function ($block) use (&$processedFields) {
319320
foreach ($block as $key => $values) {
321+
if (! is_array($values) || ! isset($values['data'])) {
322+
continue;
323+
}
324+
320325
$fields = $values['data'];
321326
$fields = ModelsField::whereIn('ulid', array_keys($fields))->get();
322327

src/Enums/Field.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ enum Field: string
1212
case CheckboxList = 'checkbox-list';
1313
case Color = 'color';
1414
case DateTime = 'date-time';
15-
// case File = 'file-upload';
15+
case File = 'file-upload';
1616
// case Hidden = 'hidden';
1717
case KeyValue = 'key-value';
1818
// case Link = 'link';

src/Fields/FileUpload.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
3+
namespace Backstage\Fields\Fields;
4+
5+
use Backstage\Fields\Contracts\FieldContract;
6+
use Backstage\Fields\Models\Field;
7+
use Filament\Forms\Components\FileUpload as FilamentFileUpload;
8+
use Filament\Forms\Components\Select;
9+
use Filament\Forms\Components\TextInput;
10+
use Filament\Forms\Components\Toggle;
11+
use Filament\Schemas\Components\Grid;
12+
use Filament\Schemas\Components\Tabs;
13+
use Filament\Schemas\Components\Tabs\Tab;
14+
use Filament\Schemas\Components\Utilities\Get;
15+
use Illuminate\Database\Eloquent\Model;
16+
17+
class FileUpload extends Base implements FieldContract
18+
{
19+
public static function getDefaultConfig(): array
20+
{
21+
return [
22+
...parent::getDefaultConfig(),
23+
'disk' => 'public',
24+
'directory' => 'uploads',
25+
'visibility' => 'public',
26+
'acceptedFileTypes' => null,
27+
'maxSize' => null,
28+
'maxFiles' => 1,
29+
'multiple' => false,
30+
'appendFiles' => false,
31+
'reorderable' => false,
32+
'openable' => true,
33+
'downloadable' => true,
34+
'previewable' => true,
35+
'deletable' => true,
36+
];
37+
}
38+
39+
public static function make(string $name, ?Field $field = null): FilamentFileUpload
40+
{
41+
$config = array_merge(self::getDefaultConfig(), $field->config ?? []);
42+
43+
$component = FilamentFileUpload::make($name)
44+
->label($field->name ?? null)
45+
->disk($config['disk'])
46+
->directory($config['directory'])
47+
->visibility($config['visibility'])
48+
->maxFiles($config['maxFiles'])
49+
->multiple($config['multiple'])
50+
->appendFiles($config['appendFiles'])
51+
->reorderable($config['reorderable'])
52+
->openable($config['openable'])
53+
->downloadable($config['downloadable'])
54+
->previewable($config['previewable'])
55+
->deletable($config['deletable']);
56+
57+
if ($config['acceptedFileTypes']) {
58+
$component->acceptedFileTypes(explode(',', $config['acceptedFileTypes']));
59+
}
60+
61+
if ($config['maxSize']) {
62+
$component->maxSize($config['maxSize']);
63+
}
64+
65+
return self::applyDefaultSettings($component, $field);
66+
}
67+
68+
public static function mutateFormDataCallback(Model $record, Field $field, array $data): array
69+
{
70+
if (! property_exists($record, 'valueColumn') || ! isset($record->values[$field->ulid])) {
71+
return $data;
72+
}
73+
74+
$data[$record->valueColumn][$field->ulid] = self::decodeFileValueForForm($record->values[$field->ulid]);
75+
76+
return $data;
77+
}
78+
79+
public static function mutateBeforeSaveCallback(Model $record, Field $field, array $data): array
80+
{
81+
if (! property_exists($record, 'valueColumn') || ! isset($data[$record->valueColumn][$field->ulid])) {
82+
return $data;
83+
}
84+
85+
$data[$record->valueColumn][$field->ulid] = self::normalizeFileValue($data[$record->valueColumn][$field->ulid]);
86+
87+
return $data;
88+
}
89+
90+
private static function decodeFileValueForForm(mixed $value): array
91+
{
92+
if (is_null($value) || $value === '') {
93+
return [];
94+
}
95+
96+
if (is_array($value)) {
97+
return $value;
98+
}
99+
100+
if (is_string($value) && json_validate($value)) {
101+
$decoded = json_decode($value, true);
102+
103+
return is_array($decoded) ? $decoded : [];
104+
}
105+
106+
if (is_string($value) && ! empty($value)) {
107+
return [$value];
108+
}
109+
110+
if (! empty($value)) {
111+
return [(string) $value];
112+
}
113+
114+
return [];
115+
}
116+
117+
private static function normalizeFileValue(mixed $value): ?string
118+
{
119+
if (is_null($value) || $value === '') {
120+
return null;
121+
}
122+
123+
if (is_array($value)) {
124+
return json_encode($value);
125+
}
126+
127+
if (is_string($value) && json_validate($value)) {
128+
return $value;
129+
}
130+
131+
if (is_string($value) && ! empty($value)) {
132+
return json_encode([$value]);
133+
}
134+
135+
if (! empty($value)) {
136+
return json_encode([(string) $value]);
137+
}
138+
139+
return null;
140+
}
141+
142+
public function getForm(): array
143+
{
144+
return [
145+
Tabs::make()
146+
->schema([
147+
Tab::make('General')
148+
->label(__('General'))
149+
->schema([
150+
...parent::getForm(),
151+
]),
152+
Tab::make('Field specific')
153+
->label(__('Field specific'))
154+
->schema([
155+
Grid::make(2)
156+
->schema([
157+
TextInput::make('config.disk')
158+
->label(__('Storage Disk'))
159+
->default('public')
160+
->required(),
161+
162+
TextInput::make('config.directory')
163+
->label(__('Upload Directory'))
164+
->default('uploads')
165+
->required(),
166+
167+
Select::make('config.visibility')
168+
->label(__('File Visibility'))
169+
->options([
170+
'public' => __('Public'),
171+
'private' => __('Private'),
172+
])
173+
->default('public')
174+
->required(),
175+
176+
TextInput::make('config.acceptedFileTypes')
177+
->label(__('Accepted File Types'))
178+
->placeholder('image/*,application/pdf')
179+
->helperText(__('Comma-separated list of MIME types or file extensions')),
180+
181+
TextInput::make('config.maxSize')
182+
->label(__('Max File Size (KB)'))
183+
->numeric()
184+
->minValue(1),
185+
186+
TextInput::make('config.maxFiles')
187+
->label(__('Max Files'))
188+
->numeric()
189+
->minValue(1)
190+
->default(1)
191+
->required(),
192+
]),
193+
194+
Grid::make(2)
195+
->schema([
196+
Toggle::make('config.multiple')
197+
->label(__('Multiple Files'))
198+
->helperText(__('Allow multiple file selection'))
199+
->live(),
200+
201+
Toggle::make('config.appendFiles')
202+
->label(__('Append Files'))
203+
->helperText(__('Append new files to existing ones'))
204+
->visible(fn (Get $get): bool => $get('config.multiple')),
205+
206+
Toggle::make('config.reorderable')
207+
->label(__('Reorderable'))
208+
->helperText(__('Allow reordering of files'))
209+
->visible(fn (Get $get): bool => $get('config.multiple')),
210+
211+
Toggle::make('config.openable')
212+
->label(__('Openable'))
213+
->helperText(__('Allow opening files in new tab')),
214+
215+
Toggle::make('config.downloadable')
216+
->label(__('Downloadable'))
217+
->helperText(__('Allow downloading files')),
218+
219+
Toggle::make('config.previewable')
220+
->label(__('Previewable'))
221+
->helperText(__('Allow previewing files')),
222+
223+
Toggle::make('config.deletable')
224+
->label(__('Deletable'))
225+
->helperText(__('Allow deleting files')),
226+
]),
227+
]),
228+
])->columnSpanFull(),
229+
];
230+
}
231+
}

0 commit comments

Comments
 (0)