Skip to content

Commit a1ebfd0

Browse files
committed
add Translatable attribute for fieldTypeConfig
1 parent 1b7b064 commit a1ebfd0

9 files changed

Lines changed: 205 additions & 123 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace SolutionForest\InspireCms\Fields\Configs\Attributes;
4+
5+
use Attribute;
6+
7+
/**
8+
* @property bool $translatable Whether the field is translatable.
9+
*/
10+
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
11+
class Translatable
12+
{
13+
public function __construct(
14+
public bool $translatable = true,
15+
) { }
16+
}

src/Fields/Configs/Concerns/HasInnerField.php

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,127 @@
22

33
namespace SolutionForest\InspireCms\Fields\Configs\Concerns;
44

5+
use Filament\Forms;
6+
use Illuminate\Support\Str;
7+
use SolutionForest\InspireCms\Helpers\FieldTypeHelper;
8+
59
trait HasInnerField
610
{
7-
protected array $fieldVariable = [];
11+
protected static array $exceptsInnerFields = [
12+
'repeater',
13+
];
14+
15+
protected static function getHasInnerFieldComponent()
16+
{
17+
$getItemKeyForRepeaterAction = fn (array $arguments): string => $arguments['item'];
18+
19+
$getItemStateForRepeaterAction = fn (array $arguments, Forms\Components\Repeater $component): array => $component->getRawItemState($getItemKeyForRepeaterAction($arguments));
20+
21+
$getFieldForRepeaterAction = fn (array $arguments, Forms\Components\Repeater $component): ?string => $getItemStateForRepeaterAction($arguments, $component)['field'] ?? null;
22+
23+
$getFieldIconForRepeaterAction = function (array $arguments, Forms\Components\Repeater $component) use ($getFieldForRepeaterAction): ?string {
24+
if (($field = $getFieldForRepeaterAction($arguments, $component)) && ($icons = FieldTypeHelper::getFieldTypeIcon($field))) {
25+
return is_array($icons) ? $icons[0] : $icons;
26+
}
27+
28+
return null;
29+
};
30+
31+
return Forms\Components\Repeater::make('fields')
32+
->columnSpanFull()
33+
->collapsible()
34+
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
35+
->addActionLabel('Add Field')
36+
->schema(static::getHasInnerFieldFieldsSchema())
37+
->defaultItems(1)
38+
->extraItemActions([
39+
Forms\Components\Actions\Action::make('editConfig')
40+
->icon('heroicon-s-cog-8-tooth')
41+
->slideOver()
42+
// todo: add translation
43+
->modalHeading(fn (array $arguments, Forms\Components\Repeater $component) => str_replace([':field'], [$getFieldForRepeaterAction($arguments, $component) ?? 'Field'], 'Edit :field configuration'))
44+
->modalIcon(fn (array $arguments, Forms\Components\Repeater $component) => $getFieldIconForRepeaterAction($arguments, $component))
45+
->disabled(fn (array $arguments, Forms\Components\Repeater $component) => empty($getFieldForRepeaterAction($arguments, $component)))
46+
->form(function (Forms\Form $form, array $arguments, Forms\Components\Repeater $component) use ($getFieldForRepeaterAction) {
47+
48+
$innerFieldTypeName = $getFieldForRepeaterAction($arguments, $component);
49+
50+
if (filled($innerFieldTypeName) &&
51+
($fieldTypeConfig = FieldTypeHelper::getFieldTypeConfig($innerFieldTypeName))
52+
) {
53+
if ($fieldTypeConfig->isFieldTypeTranslatable()) {
54+
// display "translatable" field for the field type
55+
return $fieldTypeConfig->getEnhancedFormSchema();
56+
} else {
57+
return $fieldTypeConfig->getFormSchema();
58+
}
59+
}
60+
61+
return [];
62+
})
63+
->fillForm(function (array $arguments, Forms\Components\Repeater $component) use ($getItemStateForRepeaterAction) {
64+
$existingFieldConfig = $getItemStateForRepeaterAction($arguments, $component)['fieldConfig'];
65+
if (! empty($existingFieldConfig)) {
66+
return $existingFieldConfig;
67+
}
68+
})
69+
->action(function (array $data, array $arguments, Forms\Components\Repeater $component) use ($getItemKeyForRepeaterAction) {
70+
71+
$itemKey = $getItemKeyForRepeaterAction($arguments);
72+
73+
$itemState = $component->getRawItemState($itemKey);
874

9-
public function setFieldVariable(array $variable): static
75+
$itemState['fieldConfig'] = $data;
76+
77+
$component->getChildComponentContainer($itemKey)->fill($itemState);
78+
79+
$component->collapsed(false, shouldMakeComponentCollapsible: false);
80+
81+
$component->callAfterStateUpdated();
82+
}),
83+
]);
84+
}
85+
86+
protected static function getHasInnerFieldFieldsSchema(): array
1087
{
11-
$this->fieldVariable = $variable;
88+
return [
89+
Forms\Components\Hidden::make('fieldConfig')
90+
->dehydrated()
91+
->dehydrateStateUsing(fn ($state) => $state ?? []),
92+
93+
Forms\Components\Select::make('field')
94+
->options(fn () => FieldTypeHelper::getFieldTypeOptions(excepts: static::getExceptsInnerFields()))
95+
->getSearchResultsUsing(fn ($search) => FieldTypeHelper::getFieldTypeOptions($search, excepts: static::getExceptsInnerFields()))
96+
->searchable()->allowHtml()
97+
->required()
98+
->live()
99+
// todo: add translation
100+
->hintIcon('heroicon-o-information-circle', 'Resetting the field type will clear the field configuration.')
101+
->afterStateUpdated(function ($old, $state, $set) {
102+
if ($old !== $state) {
103+
$set('fieldConfig', []);
104+
}
105+
}),
12106

13-
return $this;
107+
Forms\Components\TextInput::make('label')
108+
->required()
109+
->helperText('Label for the field')
110+
->live()->afterStateUpdated(fn ($state, $set) => $state ? $set('name', Str::slug($state)) : null),
111+
112+
Forms\Components\TextInput::make('name')
113+
->required()
114+
->helperText('Unique name for the field'),
115+
116+
Forms\Components\TextInput::make('helperText'),
117+
118+
Forms\Components\Toggle::make('isRequired')
119+
->label('Is Required?')
120+
->default(false),
121+
];
122+
}
123+
124+
protected static function getExceptsInnerFields(): array
125+
{
126+
return static::$exceptsInnerFields;
14127
}
15128
}

src/Fields/Configs/Repeater.php

Lines changed: 24 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
namespace SolutionForest\InspireCms\Fields\Configs;
44

55
use Filament\Forms;
6-
use Illuminate\Support\Str;
76
use SolutionForest\FilamentFieldGroup\FieldTypes\Configs\Attributes\ConfigName;
87
use SolutionForest\FilamentFieldGroup\FieldTypes\Configs\Attributes\DbType;
98
use SolutionForest\FilamentFieldGroup\FieldTypes\Configs\Attributes\FormComponent;
109
use SolutionForest\FilamentFieldGroup\FieldTypes\Configs\Contracts\FieldTypeConfig;
1110
use SolutionForest\FilamentFieldGroup\FieldTypes\Configs\FieldTypeBaseConfig;
1211
use SolutionForest\InspireCms\Fields\Configs\Attributes\Converter;
12+
use SolutionForest\InspireCms\Fields\Configs\Attributes\Translatable;
13+
use SolutionForest\InspireCms\Fields\Configs\Concerns\HasInnerField;
1314
use SolutionForest\InspireCms\Fields\Converters\RepeaterConverter;
1415
use SolutionForest\InspireCms\Helpers\FieldTypeHelper;
1516

@@ -18,98 +19,31 @@
1819
#[DbType('mysql', 'text')]
1920
#[DbType('sqlite', 'text')]
2021
#[Converter(RepeaterConverter::class)]
22+
#[Translatable(false)]
2123
class Repeater extends FieldTypeBaseConfig implements FieldTypeConfig
2224
{
23-
public array $fields = [];
24-
25-
public function getFormSchema(): array
26-
{
27-
$exceptsInnerFields = [
28-
'repeater',
29-
];
25+
use HasInnerField;
3026

31-
$getItemKeyForRepeaterAction = fn (array $arguments): string => $arguments['item'];
32-
33-
$getItemStateForRepeaterAction = fn (array $arguments, Forms\Components\Repeater $component): array => $component->getRawItemState($getItemKeyForRepeaterAction($arguments));
27+
public array $fields = [];
3428

35-
$getFieldForRepeaterAction = fn (array $arguments, Forms\Components\Repeater $component): ?string => $getItemStateForRepeaterAction($arguments, $component)['field'] ?? null;
29+
public bool $collapsible = false;
3630

37-
$getFieldIconForRepeaterAction = function (array $arguments, Forms\Components\Repeater $component) use ($getFieldForRepeaterAction): ?string {
38-
if (($field = $getFieldForRepeaterAction($arguments, $component)) && ($icons = FieldTypeHelper::getFieldTypeIcon($field))) {
39-
return is_array($icons) ? $icons[0] : $icons;
40-
}
41-
42-
return null;
43-
};
31+
public bool $cloneable = false;
4432

33+
public function getFormSchema(): array
34+
{
4535
return [
46-
Forms\Components\Repeater::make('fields')
47-
->columnSpanFull()
48-
->collapsible()
49-
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
50-
->addActionLabel('Add Field')
51-
->schema(function ($state) use ($exceptsInnerFields) {
52-
return [
53-
Forms\Components\Hidden::make('fieldConfig')->dehydrated()->dehydrateStateUsing(fn ($state) => $state ?? []),
54-
Forms\Components\Select::make('field')
55-
->options(fn () => FieldTypeHelper::getFieldTypeOptions(excepts: $exceptsInnerFields))
56-
->getSearchResultsUsing(fn ($search) => FieldTypeHelper::getFieldTypeOptions($search, excepts: $exceptsInnerFields))
57-
->searchable()->allowHtml()
58-
->required()
59-
->live()
60-
// todo: add translation
61-
->hintIcon('heroicon-o-information-circle', 'Resetting the field type will clear the field configuration.')
62-
->afterStateUpdated(function ($old, $state, $set) {
63-
if ($old !== $state) {
64-
$set('fieldConfig', []);
65-
}
66-
}),
67-
Forms\Components\TextInput::make('label')
68-
->required()
69-
->helperText('Label for the field')
70-
->live()->afterStateUpdated(fn ($state, $set) => $state ? $set('name', Str::slug($state)) : null),
71-
Forms\Components\TextInput::make('name')
72-
->required()
73-
->helperText('Unique name for the field'),
74-
Forms\Components\TextInput::make('helperText'),
75-
Forms\Components\Toggle::make('isRequired')
76-
->label('Is Required?')
77-
->default(false),
78-
];
79-
})
80-
->defaultItems(1)
81-
->extraItemActions([
82-
Forms\Components\Actions\Action::make('editConfig')
83-
->icon('heroicon-s-cog-8-tooth')
84-
->slideOver()
85-
// todo: add translation
86-
->modalHeading(fn (array $arguments, Forms\Components\Repeater $component) => str_replace([':field'], [$getFieldForRepeaterAction($arguments, $component) ?? 'Field'], 'Edit :field configuration'))
87-
->modalIcon(fn (array $arguments, Forms\Components\Repeater $component) => $getFieldIconForRepeaterAction($arguments, $component))
88-
->disabled(fn (array $arguments, Forms\Components\Repeater $component) => empty($getFieldForRepeaterAction($arguments, $component)))
89-
->form(
90-
fn (Forms\Form $form, array $arguments, Forms\Components\Repeater $component) => $form
91-
->schema(FieldTypeHelper::getRepeaterFieldConfigSchemaForFieldType($getFieldForRepeaterAction($arguments, $component)))
92-
)
93-
->fillForm(function (array $arguments, Forms\Components\Repeater $component) use ($getItemStateForRepeaterAction) {
94-
$existingFieldConfig = $getItemStateForRepeaterAction($arguments, $component)['fieldConfig'];
95-
if (! empty($existingFieldConfig)) {
96-
return $existingFieldConfig;
97-
}
98-
})
99-
->action(function (array $data, array $arguments, Forms\Components\Repeater $component) use ($getItemKeyForRepeaterAction) {
100-
101-
$itemKey = $getItemKeyForRepeaterAction($arguments);
102-
103-
$itemState = $component->getRawItemState($itemKey);
104-
105-
$itemState['fieldConfig'] = $data;
106-
107-
$component->getChildComponentContainer($itemKey)->fill($itemState);
108-
109-
$component->collapsed(false, shouldMakeComponentCollapsible: false);
110-
111-
$component->callAfterStateUpdated();
112-
}),
36+
Forms\Components\Tabs::make('tabs')
37+
->tabs([
38+
Forms\Components\Tabs\Tab::make('Presentation')
39+
->schema([
40+
Forms\Components\Toggle::make('collapsible'),
41+
Forms\Components\Toggle::make('cloneable'),
42+
]),
43+
Forms\Components\Tabs\Tab::make('Fields')
44+
->schema([
45+
static::getHasInnerFieldComponent()->hiddenLabel(),
46+
]),
11347
]),
11448
];
11549
}
@@ -140,6 +74,10 @@ public function applyConfig(Forms\Components\Component $component): void
14074
);
14175
}
14276
$component->schema(array_filter($components));
77+
78+
$component->collapsible($this->collapsible);
79+
80+
$component->cloneable($this->cloneable);
14381
}
14482
}
14583
}

src/Fields/Mixins/FieldTypeConverter.php

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace SolutionForest\InspireCms\Fields\Mixins;
44

5-
use ReflectionClass;
6-
use ReflectionAttribute;
75
use SolutionForest\InspireCms\Fields\Configs\Attributes\Converter;
86
use SolutionForest\InspireCms\Fields\Converters\DefaultConverter;
97

@@ -15,22 +13,4 @@ public function getConverter()
1513
->map(fn (Converter $attribute) => $attribute->converter)
1614
->first() ?? DefaultConverter::class;
1715
}
18-
19-
public function getAttributes()
20-
{
21-
return function () {
22-
$reflection = new ReflectionClass(static::class);
23-
24-
return $reflection->getAttributes();
25-
};
26-
}
27-
28-
public function getTargetAttributes()
29-
{
30-
return fn (string $attributeName) => collect($this->getAttributes())
31-
->whereInstanceOf(ReflectionAttribute::class)
32-
->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeName)
33-
->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance())
34-
->all();
35-
}
3616
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace SolutionForest\InspireCms\Fields\Mixins;
4+
5+
use ReflectionClass;
6+
use ReflectionAttribute;
7+
8+
class SimpleFieldDefinition
9+
{
10+
public function getAttributes()
11+
{
12+
return function () {
13+
$reflection = new ReflectionClass(static::class);
14+
15+
return $reflection->getAttributes();
16+
};
17+
}
18+
19+
public function getTargetAttributes()
20+
{
21+
return fn (string $attributeName) => collect($this->getAttributes())
22+
->whereInstanceOf(ReflectionAttribute::class)
23+
->filter(fn (ReflectionAttribute $attribute) => $attribute->getName() === $attributeName)
24+
->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance())
25+
->all();
26+
}
27+
}

src/Fields/Mixins/TranslatableFieldType.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Filament\Forms\Components\Section;
66
use Filament\Forms\Components\Toggle;
7+
use SolutionForest\InspireCms\Fields\Configs\Attributes\Translatable;
78

89
class TranslatableFieldType
910
{
@@ -31,4 +32,19 @@ public function isTranslatable()
3132
return isset($this->translatable) && $this->translatable === true;
3233
};
3334
}
35+
36+
public function isFieldTypeTranslatable()
37+
{
38+
return function () {
39+
$translatable = collect($this->getTargetAttributes(Translatable::class))
40+
->map(fn (Translatable $attribute) => $attribute->translatable)
41+
->first();
42+
43+
if (is_null($translatable)) {
44+
return true;
45+
}
46+
47+
return $translatable;
48+
};
49+
}
3450
}

0 commit comments

Comments
 (0)