Skip to content

Commit cfbed9c

Browse files
committed
Merge branch 'main' of github.com:backstagephp/fields into release/1.x
2 parents b63be20 + e1ac1ec commit cfbed9c

File tree

8 files changed

+282
-27
lines changed

8 files changed

+282
-27
lines changed

.github/workflows/fix-php-code-style-issues.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
ref: ${{ github.head_ref }}
2020

2121
- name: Fix PHP code style issues
22-
uses: aglipanci/laravel-pint-action@2.5
22+
uses: aglipanci/laravel-pint-action@2.6
2323

2424
- name: Commit changes
2525
uses: stefanzweifel/git-auto-commit-action@v6

README.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,105 @@ return [
232232

233233
### Creating your own fields
234234

235-
...
235+
To create your own custom fields, you need to extend the `Base` field class and implement the required methods. Here's an example of a custom field:
236+
237+
```php
238+
<?php
239+
240+
namespace App\Fields;
241+
242+
use Backstage\Fields\Fields\Base;
243+
use Filament\Forms\Components\TextInput;
244+
245+
class CustomField extends Base
246+
{
247+
public static function make(string $name, ?Field $field = null): TextInput
248+
{
249+
$input = self::applyDefaultSettings(TextInput::make($name), $field);
250+
251+
// Add your custom field logic here
252+
$input->placeholder('Custom placeholder');
253+
254+
return $input;
255+
}
256+
257+
public function getForm(): array
258+
{
259+
return [
260+
// Your custom form configuration
261+
TextInput::make('config.customOption')
262+
->label('Custom Option'),
263+
];
264+
}
265+
266+
public static function getDefaultConfig(): array
267+
{
268+
return [
269+
...parent::getDefaultConfig(),
270+
'customOption' => null,
271+
];
272+
}
273+
}
274+
```
275+
276+
#### Excluding base fields from custom fields
277+
278+
When creating custom fields, you may want to exclude certain base fields that don't apply to your field type. For example, a Repeater field doesn't need a "Default value" field since it's a container for other fields.
279+
280+
You can exclude base fields by overriding the `excludeFromBaseSchema()` method:
281+
282+
```php
283+
<?php
284+
285+
namespace App\Fields;
286+
287+
use Backstage\Fields\Fields\Base;
288+
289+
class RepeaterField extends Base
290+
{
291+
// Exclude the default value field since it doesn't make sense for repeaters
292+
protected function excludeFromBaseSchema(): array
293+
{
294+
return ['defaultValue'];
295+
}
296+
297+
// Your field implementation...
298+
}
299+
```
300+
301+
Available base fields that can be excluded:
302+
- `required` - Required field toggle
303+
- `disabled` - Disabled field toggle
304+
- `hidden` - Hidden field toggle
305+
- `helperText` - Helper text input
306+
- `hint` - Hint text input
307+
- `hintColor` - Hint color picker
308+
- `hintIcon` - Hint icon input
309+
- `defaultValue` - Default value input
310+
311+
#### Best practices for field exclusion
312+
313+
- **Only exclude what doesn't apply**: Don't exclude fields just because you don't use them - only exclude fields that conceptually don't make sense for your field type
314+
- **Document your exclusions**: Add comments explaining why certain fields are excluded
315+
- **Test thoroughly**: Make sure your field still works correctly after excluding base fields
316+
- **Consider inheritance**: If your field extends another custom field, make sure to call `parent::excludeFromBaseSchema()` if you need to add more exclusions
317+
318+
Example of a field that excludes multiple base fields:
319+
320+
```php
321+
class ImageField extends Base
322+
{
323+
protected function excludeFromBaseSchema(): array
324+
{
325+
return [
326+
'defaultValue', // Images don't have default values
327+
'hint', // Image fields typically don't need hints
328+
'hintColor', // No hint means no hint color
329+
'hintIcon', // No hint means no hint icon
330+
];
331+
}
332+
}
333+
```
236334

237335
### Registering your own fields
238336

src/Concerns/CanMapDynamicFields.php

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Backstage\Fields\Fields\Color;
1111
use Backstage\Fields\Fields\DateTime;
1212
use Backstage\Fields\Fields\KeyValue;
13+
use Backstage\Fields\Fields\MarkdownEditor;
1314
use Backstage\Fields\Fields\Radio;
1415
use Backstage\Fields\Fields\Repeater;
1516
use Backstage\Fields\Fields\RichEditor;
@@ -41,6 +42,7 @@ trait CanMapDynamicFields
4142
'text' => Text::class,
4243
'textarea' => Textarea::class,
4344
'rich-editor' => RichEditor::class,
45+
'markdown-editor' => MarkdownEditor::class,
4446
'repeater' => Repeater::class,
4547
'select' => Select::class,
4648
'checkbox' => Checkbox::class,
@@ -110,6 +112,7 @@ protected function mutateBeforeSave(array $data): array
110112
}
111113

112114
$builderBlocks = $this->extractBuilderBlocks($values);
115+
113116
$allFields = $this->getAllFieldsIncludingBuilderFields($builderBlocks);
114117

115118
return $this->mutateFormData($data, $allFields, function ($field, $fieldConfig, $fieldInstance, $data) use ($builderBlocks) {
@@ -485,6 +488,8 @@ private function determineFieldLocation(Model $field, array $builderBlocks): arr
485488
return [
486489
'isInBuilder' => true,
487490
'builderData' => $block['data'],
491+
'builderUlid' => $builderUlid,
492+
'blockIndex' => array_search($block, $builderBlocks),
488493
];
489494
}
490495
}
@@ -494,6 +499,8 @@ private function determineFieldLocation(Model $field, array $builderBlocks): arr
494499
return [
495500
'isInBuilder' => false,
496501
'builderData' => null,
502+
'builderUlid' => null,
503+
'blockIndex' => null,
497504
];
498505
}
499506

@@ -512,17 +519,25 @@ private function determineFieldLocation(Model $field, array $builderBlocks): arr
512519
*/
513520
private function processBuilderFieldMutation(Model $field, object $fieldInstance, array $data, array $builderData, array $builderBlocks): array
514521
{
515-
// Create a mock record with the builder data for the callback
516-
$mockRecord = $this->createMockRecordForBuilder($builderData);
522+
foreach ($builderBlocks as $builderUlid => &$blocks) {
523+
if (is_array($blocks)) {
524+
foreach ($blocks as &$block) {
525+
if (isset($block['data']) && is_array($block['data']) && isset($block['data'][$field->ulid])) {
526+
// Create a mock record with the block data for the callback
527+
$mockRecord = $this->createMockRecordForBuilder($block['data']);
517528

518-
// Create a temporary data structure for the callback
519-
$tempData = [$this->record->valueColumn => $builderData];
520-
$tempData = $fieldInstance->mutateBeforeSaveCallback($mockRecord, $field, $tempData);
529+
// Create a temporary data structure for the callback
530+
$tempData = [$this->record->valueColumn => $block['data']];
531+
$tempData = $fieldInstance->mutateBeforeSaveCallback($mockRecord, $field, $tempData);
521532

522-
// Update the original data structure with the mutated values
523-
$this->updateBuilderBlocksWithMutatedData($builderBlocks, $field, $tempData);
533+
if (isset($tempData[$this->record->valueColumn][$field->ulid])) {
534+
$block['data'][$field->ulid] = $tempData[$this->record->valueColumn][$field->ulid];
535+
}
536+
}
537+
}
538+
}
539+
}
524540

525-
// Update the main data structure
526541
$data[$this->record->valueColumn] = array_merge($data[$this->record->valueColumn], $builderBlocks);
527542

528543
return $data;

src/Enums/Field.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ enum Field: string
1212
case CheckboxList = 'checkbox-list';
1313
case Color = 'color';
1414
case DateTime = 'date-time';
15-
case File = 'file-upload';
16-
case Hidden = 'hidden';
15+
// case File = 'file-upload';
16+
// case Hidden = 'hidden';
1717
case KeyValue = 'key-value';
18-
case Link = 'link';
18+
// case Link = 'link';
1919
case MarkdownEditor = 'markdown-editor';
2020
case Radio = 'radio';
2121
case Repeater = 'repeater';
@@ -25,5 +25,5 @@ enum Field: string
2525
case Text = 'text';
2626
case Textarea = 'textarea';
2727
case Toggle = 'toggle';
28-
case ToggleButtons = 'toggle-buttons';
28+
// case ToggleButtons = 'toggle-buttons';
2929
}

src/Fields/Base.php

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ abstract class Base implements FieldContract
1515
{
1616
public function getForm(): array
1717
{
18-
return [
18+
return $this->getBaseFormSchema();
19+
}
20+
21+
protected function getBaseFormSchema(): array
22+
{
23+
$schema = [
1924
Grid::make(3)
2025
->schema([
2126
Toggle::make('config.required')
@@ -52,7 +57,56 @@ public function getForm(): array
5257
return ! empty(trim($hint));
5358
}),
5459
]),
60+
TextInput::make('config.defaultValue')
61+
->label(__('Default value'))
62+
->helperText(__('This value will be used when creating new records.')),
5563
];
64+
65+
return $this->filterExcludedFields($schema);
66+
}
67+
68+
protected function excludeFromBaseSchema(): array
69+
{
70+
return [];
71+
}
72+
73+
private function filterExcludedFields(array $schema): array
74+
{
75+
$excluded = $this->excludeFromBaseSchema();
76+
77+
if (empty($excluded)) {
78+
return $schema;
79+
}
80+
81+
return array_filter($schema, function ($field) use ($excluded) {
82+
foreach ($excluded as $excludedField) {
83+
if ($this->fieldContainsConfigKey($field, $excludedField)) {
84+
return false;
85+
}
86+
}
87+
88+
return true;
89+
});
90+
}
91+
92+
private function fieldContainsConfigKey($field, string $configKey): bool
93+
{
94+
$reflection = new \ReflectionObject($field);
95+
$propertiesToCheck = ['name', 'statePath'];
96+
97+
foreach ($propertiesToCheck as $propertyName) {
98+
if ($reflection->hasProperty($propertyName)) {
99+
$property = $reflection->getProperty($propertyName);
100+
$property->setAccessible(true);
101+
$value = $property->getValue($field);
102+
103+
if (str_contains($value, "config.{$configKey}")) {
104+
return true;
105+
}
106+
}
107+
}
108+
109+
return false;
56110
}
57111

58112
public static function getDefaultConfig(): array
@@ -65,6 +119,7 @@ public static function getDefaultConfig(): array
65119
'hint' => null,
66120
'hintColor' => null,
67121
'hintIcon' => null,
122+
'defaultValue' => null,
68123
];
69124
}
70125

@@ -82,6 +137,10 @@ public static function applyDefaultSettings($input, ?Field $field = null)
82137
$input->hintColor(Color::generateV3Palette($field->config['hintColor']));
83138
}
84139

140+
if (isset($field->config['defaultValue']) && $field->config['defaultValue'] !== null) {
141+
$input->default($field->config['defaultValue']);
142+
}
143+
85144
return $input;
86145
}
87146

src/Fields/MarkdownEditor.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Backstage\Fields\Fields;
4+
5+
use Backstage\Fields\Contracts\FieldContract;
6+
use Backstage\Fields\Enums\ToolbarButton;
7+
use Backstage\Fields\Models\Field;
8+
use Filament\Forms;
9+
use Filament\Forms\Components\RichEditor as Input;
10+
11+
class MarkdownEditor extends Base implements FieldContract
12+
{
13+
public static function getDefaultConfig(): array
14+
{
15+
return [
16+
...parent::getDefaultConfig(),
17+
'toolbarButtons' => ['attachFiles', 'blockquote', 'bold', 'bulletList', 'codeBlock', 'heading', 'italic', 'link', 'orderedList', 'redo', 'strike', 'table', 'undo'],
18+
'disableToolbarButtons' => [],
19+
'fileAttachmentsDisk' => 'public',
20+
'fileAttachmentsDirectory' => 'attachments',
21+
'fileAttachmentsVisibility' => 'public',
22+
];
23+
}
24+
25+
public static function make(string $name, ?Field $field = null): Input
26+
{
27+
$input = self::applyDefaultSettings(Input::make($name), $field);
28+
29+
$input = $input->label($field->name ?? null)
30+
->toolbarButtons($field->config['toolbarButtons'] ?? self::getDefaultConfig()['toolbarButtons'])
31+
->disableToolbarButtons($field->config['disableToolbarButtons'] ?? self::getDefaultConfig()['disableToolbarButtons'])
32+
->fileAttachmentsDisk($field->config['fileAttachmentsDisk'] ?? self::getDefaultConfig()['fileAttachmentsDisk'])
33+
->fileAttachmentsDirectory($field->config['fileAttachmentsDirectory'] ?? self::getDefaultConfig()['fileAttachmentsDirectory'])
34+
->fileAttachmentsVisibility($field->config['fileAttachmentsVisibility'] ?? self::getDefaultConfig()['fileAttachmentsVisibility']);
35+
36+
return $input;
37+
}
38+
39+
public function getForm(): array
40+
{
41+
return [
42+
Forms\Components\Tabs::make()
43+
->schema([
44+
Forms\Components\Tabs\Tab::make('General')
45+
->label(__('General'))
46+
->schema([
47+
...parent::getForm(),
48+
]),
49+
Forms\Components\Tabs\Tab::make('Field specific')
50+
->label(__('Field specific'))
51+
->schema([
52+
Forms\Components\Grid::make(2)
53+
->schema([
54+
Forms\Components\Select::make('config.toolbarButtons')
55+
->label(__('Toolbar buttons'))
56+
->default(['attachFiles', 'blockquote', 'bold', 'bulletList', 'codeBlock', 'heading', 'italic', 'link', 'orderedList', 'redo', 'strike', 'table', 'undo'])
57+
->default(ToolbarButton::array()) // Not working in Filament yet.
58+
->multiple()
59+
->options(ToolbarButton::array())
60+
->columnSpanFull(),
61+
Forms\Components\Grid::make(3)
62+
->schema([
63+
Forms\Components\TextInput::make('config.fileAttachmentsDisk')
64+
->label(__('File attachments disk'))
65+
->default('public'),
66+
Forms\Components\TextInput::make('config.fileAttachmentsDirectory')
67+
->label(__('File attachments directory'))
68+
->default('attachments'),
69+
Forms\Components\TextInput::make('config.fileAttachmentsVisibility')
70+
->label(__('File attachments visibility'))
71+
->default('public'),
72+
]),
73+
]),
74+
]),
75+
])->columnSpanFull(),
76+
];
77+
}
78+
}

0 commit comments

Comments
 (0)