Skip to content

Commit 3cee667

Browse files
feat(products): implement custom properties (CT-33)
* feat(custom-properties): implement custom properties * chore: add proof of concept for inline translatable component * chore: improve UI for translatable fields * chore: custom properties improvements * chore: more custom property improvements * chore: custom properties improvements * chore: improve tabs * fix: resolve duplicate methods after merging main * chore: layout fixes * chore: register custom CSS file run `lando artisan filament:assets` after pulling this change --------- Co-authored-by: Omer Šabić <omer@datalinx.si>
1 parent 0c91cf6 commit 3cee667

29 files changed

Lines changed: 2853 additions & 128 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('pim_property', function (Blueprint $table) {
15+
$table->string('type')->default('list')->after('is_filter');
16+
$table->string('input_type')->nullable()->after('type');
17+
$table->boolean('is_multilang')->default(false)->after('input_type');
18+
});
19+
}
20+
21+
/**
22+
* Reverse the migrations.
23+
*/
24+
public function down(): void
25+
{
26+
Schema::table('pim_property', function (Blueprint $table) {
27+
$table->dropColumn(['type', 'input_type', 'is_multilang']);
28+
});
29+
}
30+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('catalogue_product_has_custom_prop_value', function (Blueprint $table) {
15+
$table->id();
16+
$table->foreignId('product_id')->constrained('catalogue_products')->onDelete('cascade')->onUpdate('cascade');
17+
$table->foreignId('property_id')->constrained('pim_property')->onDelete('cascade')->onUpdate('cascade');
18+
$table->text('value');
19+
$table->timestamps();
20+
$table->unique(['product_id', 'property_id'], 'product_custom_property_unique');
21+
});
22+
}
23+
24+
/**
25+
* Reverse the migrations.
26+
*/
27+
public function down(): void
28+
{
29+
Schema::dropIfExists('catalogue_product_has_custom_prop_value');
30+
}
31+
};

resources/css/catalogue.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.translatable-field-container > .grid {
2+
row-gap: .25rem !important;
3+
}

src/CatalogueServiceProvider.php

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

55
use Eclipse\Catalogue\Models\Category;
66
use Eclipse\Catalogue\Models\Product;
7+
use Filament\Support\Assets\Css;
8+
use Filament\Support\Facades\FilamentAsset;
79
use Illuminate\Support\Facades\Config;
810
use Spatie\LaravelPackageTools\Package;
911
use Spatie\LaravelPackageTools\PackageServiceProvider;
@@ -46,5 +48,9 @@ public function boot()
4648
if (class_exists(\Livewire\Livewire::class)) {
4749
\Livewire\Livewire::component('eclipse-catalogue::tenant-switcher', \Eclipse\Catalogue\Livewire\TenantSwitcher::class);
4850
}
51+
52+
FilamentAsset::register([
53+
Css::make('eclipse-catalogue', __DIR__.'/../resources/css/catalogue.css'),
54+
], package: static::$name);
4955
}
5056
}

src/Enums/PropertyInputType.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Enums;
4+
5+
enum PropertyInputType: string
6+
{
7+
case STRING = 'string';
8+
case TEXT = 'text';
9+
case INTEGER = 'integer';
10+
case DECIMAL = 'decimal';
11+
case DATE = 'date';
12+
case DATETIME = 'datetime';
13+
case FILE = 'file';
14+
15+
public function getLabel(): string
16+
{
17+
return match ($this) {
18+
self::STRING => 'String (up to 255 characters)',
19+
self::TEXT => 'Text',
20+
self::INTEGER => 'Integer',
21+
self::DECIMAL => 'Decimal',
22+
self::DATE => 'Date',
23+
self::DATETIME => 'Date & Time',
24+
self::FILE => 'File',
25+
};
26+
}
27+
28+
public function supportsMultilang(): bool
29+
{
30+
return in_array($this, [self::STRING, self::TEXT, self::FILE]);
31+
}
32+
}

src/Enums/PropertyType.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Enums;
4+
5+
enum PropertyType: string
6+
{
7+
case LIST = 'list';
8+
case CUSTOM = 'custom';
9+
10+
public function getLabel(): string
11+
{
12+
return match ($this) {
13+
self::LIST => 'List (Predefined Values)',
14+
self::CUSTOM => 'Custom (User Input)',
15+
};
16+
}
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Filament\Filters;
4+
5+
use Eclipse\Catalogue\Filament\Filters\Operators\CustomPropertyContainsOperator;
6+
use Eclipse\Catalogue\Filament\Filters\Operators\CustomPropertyEndsWithOperator;
7+
use Eclipse\Catalogue\Filament\Filters\Operators\CustomPropertyEqualsOperator;
8+
use Eclipse\Catalogue\Filament\Filters\Operators\CustomPropertyStartsWithOperator;
9+
use Eclipse\Catalogue\Models\Property;
10+
use Filament\Tables\Filters\QueryBuilder\Constraints\Constraint;
11+
12+
class CustomPropertyConstraint extends Constraint
13+
{
14+
protected Property $property;
15+
16+
/**
17+
* Create a new custom property constraint for a given property.
18+
*
19+
* @param Property $property The property to create the constraint for.
20+
* @return static The created constraint.
21+
*/
22+
public static function forProperty(Property $property): static
23+
{
24+
$static = parent::make("custom_property_{$property->id}");
25+
$static->property = $property;
26+
$label = $property->internal_name ?: (is_array($property->name)
27+
? ($property->name[app()->getLocale()] ?? reset($property->name))
28+
: $property->name);
29+
$static->label($label);
30+
31+
return $static;
32+
}
33+
34+
/**
35+
* Set up the constraint.
36+
*/
37+
protected function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
$this->operators([
42+
CustomPropertyEqualsOperator::make(),
43+
CustomPropertyContainsOperator::make(),
44+
CustomPropertyStartsWithOperator::make(),
45+
CustomPropertyEndsWithOperator::make(),
46+
]);
47+
}
48+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Filament\Filters\Operators;
4+
5+
use Filament\Forms\Components\Component;
6+
use Filament\Forms\Components\TextInput;
7+
use Filament\Tables\Filters\QueryBuilder\Constraints\Operators\Operator;
8+
use Illuminate\Database\Eloquent\Builder;
9+
10+
class CustomPropertyContainsOperator extends Operator
11+
{
12+
/**
13+
* Get the name of the operator.
14+
*/
15+
public function getName(): string
16+
{
17+
return 'contains';
18+
}
19+
20+
/**
21+
* Get the label of the operator.
22+
*/
23+
public function getLabel(): string
24+
{
25+
return __(
26+
$this->isInverse() ?
27+
'filament-tables::filters/query-builder.operators.text.contains.label.inverse' :
28+
'filament-tables::filters/query-builder.operators.text.contains.label.direct',
29+
);
30+
}
31+
32+
/**
33+
* Get the summary of the operator.
34+
*/
35+
public function getSummary(): string
36+
{
37+
return __(
38+
$this->isInverse() ?
39+
'filament-tables::filters/query-builder.operators.text.contains.summary.inverse' :
40+
'filament-tables::filters/query-builder.operators.text.contains.summary.direct',
41+
[
42+
'attribute' => $this->getConstraint()->getAttributeLabel(),
43+
'text' => $this->getSettings()['text'],
44+
],
45+
);
46+
}
47+
48+
/**
49+
* Get the form schema of the operator.
50+
*
51+
* @return array<Component>
52+
*/
53+
public function getFormSchema(): array
54+
{
55+
return [
56+
TextInput::make('text')
57+
->label(__('filament-tables::filters/query-builder.operators.text.form.text.label'))
58+
->required()
59+
->columnSpanFull(),
60+
];
61+
}
62+
63+
/**
64+
* Apply the operator to the query.
65+
*/
66+
public function apply(Builder $query, string $qualifiedColumn): Builder
67+
{
68+
$text = trim($this->getSettings()['text']);
69+
70+
if (empty($text)) {
71+
return $query;
72+
}
73+
74+
$constraintName = $this->getConstraint()->getName();
75+
$propertyId = (int) str_replace('custom_property_', '', $constraintName);
76+
77+
return $query->whereHas('customPropertyValues', function ($q) use ($propertyId, $text) {
78+
$q->where('property_id', $propertyId)
79+
->whereRaw('REGEXP_REPLACE(value, "<[^>]*>", "") LIKE ?', ["%{$text}%"]);
80+
});
81+
}
82+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Eclipse\Catalogue\Filament\Filters\Operators;
4+
5+
use Filament\Forms\Components\Component;
6+
use Filament\Forms\Components\TextInput;
7+
use Filament\Tables\Filters\QueryBuilder\Constraints\Operators\Operator;
8+
use Illuminate\Database\Eloquent\Builder;
9+
10+
class CustomPropertyEndsWithOperator extends Operator
11+
{
12+
/**
13+
* Get the name of the operator.
14+
*/
15+
public function getName(): string
16+
{
17+
return 'ends_with';
18+
}
19+
20+
/**
21+
* Get the label of the operator.
22+
*/
23+
public function getLabel(): string
24+
{
25+
return __(
26+
$this->isInverse() ?
27+
'filament-tables::filters/query-builder.operators.text.ends_with.label.inverse' :
28+
'filament-tables::filters/query-builder.operators.text.ends_with.label.direct',
29+
);
30+
}
31+
32+
/**
33+
* Get the summary of the operator.
34+
*/
35+
public function getSummary(): string
36+
{
37+
return __(
38+
$this->isInverse() ?
39+
'filament-tables::filters/query-builder.operators.text.ends_with.summary.inverse' :
40+
'filament-tables::filters/query-builder.operators.text.ends_with.summary.direct',
41+
[
42+
'attribute' => $this->getConstraint()->getAttributeLabel(),
43+
'text' => $this->getSettings()['text'],
44+
],
45+
);
46+
}
47+
48+
/**
49+
* Get the form schema of the operator.
50+
*
51+
* @return array<Component>
52+
*/
53+
public function getFormSchema(): array
54+
{
55+
return [
56+
TextInput::make('text')
57+
->label(__('filament-tables::filters/query-builder.operators.text.form.text.label'))
58+
->required()
59+
->columnSpanFull(),
60+
];
61+
}
62+
63+
/**
64+
* Apply the operator to the query.
65+
*/
66+
public function apply(Builder $query, string $qualifiedColumn): Builder
67+
{
68+
$text = trim($this->getSettings()['text']);
69+
70+
if (empty($text)) {
71+
return $query;
72+
}
73+
74+
$constraintName = $this->getConstraint()->getName();
75+
$propertyId = (int) str_replace('custom_property_', '', $constraintName);
76+
77+
return $query->whereHas('customPropertyValues', function ($q) use ($propertyId, $text) {
78+
$q->where('property_id', $propertyId)
79+
->whereRaw('REGEXP_REPLACE(value, "<[^>]*>", "") LIKE ?', ["%{$text}"]);
80+
});
81+
}
82+
}

0 commit comments

Comments
 (0)