Skip to content

Commit

Permalink
Merge pull request #1610 from dragomano/option_image
Browse files Browse the repository at this point in the history
feat: OptionImage
  • Loading branch information
lee-to authored Mar 9, 2025
2 parents dca26bb + 2ffc386 commit a62ad26
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 16 deletions.
49 changes: 49 additions & 0 deletions src/Support/src/DTOs/Select/OptionImage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace MoonShine\Support\DTOs\Select;

use Illuminate\Contracts\Support\Arrayable;
use MoonShine\Support\Enums\ObjectFit;

final readonly class OptionImage implements Arrayable
{
public function __construct(
private string $src,
private int $width = 10,
private int $height = 10,
private ObjectFit $objectFit = ObjectFit::COVER,
) {
}

public function getSrc(): string
{
return $this->src;
}

public function getWidth(): int
{
return $this->width;
}

public function getHeight(): int
{
return $this->height;
}

public function getObjectFit(): string
{
return $this->objectFit->value;
}

public function toArray(): array
{
return [
'src' => $this->getSrc(),
'width' => $this->getWidth(),
'height' => $this->getHeight(),
'objectFit' => $this->getObjectFit(),
];
}
}
16 changes: 11 additions & 5 deletions src/Support/src/DTOs/Select/OptionProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,25 @@
final readonly class OptionProperty implements Arrayable
{
public function __construct(
private ?string $image = null,
private null|string|OptionImage $image = null,
) {
}

public function getImage(): ?string
public function getImage(): null|string|OptionImage
{
return $this->image;
}

public function toArray(): array
{
return [
'image' => $this->getImage(),
];
$image = $this->getImage();

if ($image instanceof OptionImage) {
$image = $image->toArray();
}

return [
'image' => $image,
];
}
}
29 changes: 28 additions & 1 deletion src/Support/src/DTOs/Select/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use JsonException;
use MoonShine\Support\Enums\ObjectFit;
use UnitEnum;

final readonly class Options implements Arrayable
Expand Down Expand Up @@ -73,7 +74,9 @@ public function getProperties(string $value): OptionProperty
return $properties;
}

return new OptionProperty(...$properties ?? []);
$properties = $this->normalizeProperties($properties);

return new OptionProperty(...$properties);
}

/**
Expand Down Expand Up @@ -144,4 +147,28 @@ public function toRaw(): array
'properties' => $properties,
];
}

private function normalizeProperties(array $properties): array
{
if (! isset($properties['image']) || $properties['image'] instanceof OptionImage) {
return $properties;
}

$imageData = $properties['image'];

if (is_string($imageData)) {
$properties['image'] = new OptionImage($imageData);

return $properties;
}

$properties['image'] = new OptionImage(
$imageData['src'] ?? '',
$imageData['width'] ?? null,
$imageData['height'] ?? null,
isset($imageData['objectFit']) ? ObjectFit::from($imageData['objectFit']) : null
);

return $properties;
}
}
14 changes: 14 additions & 0 deletions src/Support/src/Enums/ObjectFit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace MoonShine\Support\Enums;

enum ObjectFit: string
{
case CONTAIN = 'contain';
case COVER = 'cover';
case FILL = 'fill';
case NONE = 'none';
case SCALE_DOWN = 'scale-down';
}
38 changes: 28 additions & 10 deletions src/UI/resources/js/Components/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,27 @@ export default (asyncUrl = '') => ({
},
searchResultLimit: 100,
callbackOnCreateTemplates: function (strToEl, escapeForTemplate) {
function normalizeImageData(image) {
if (typeof image === 'string') {
return {
src: image,
width: 10,
height: 10,
objectFit: 'cover'
};
}

return {
src: image?.src ?? '',
width: image?.width ?? 10,
height: image?.height ?? 10,
objectFit: image?.objectFit ?? 'cover'
};
}

return {
item: ({classNames}, data, removeItemButton) => {
const image = data.customProperties?.image
const { src: imgSrc, width, height, objectFit } = normalizeImageData(data.customProperties?.image);

return strToEl(`
<div class="${classNames.item} ${
Expand All @@ -127,10 +145,10 @@ export default (asyncUrl = '') => ({
}>
<div class="flex gap-x-2 items-center">
${
image
? '<div class="zoom-in h-10 w-10 overflow-hidden rounded-md">' +
'<img class="h-full w-full object-cover" src="' +
escapeForTemplate(this.config.allowHTML, image) +
imgSrc
? '<div class="zoom-in h-' + height + ' w-' + width + ' overflow-hidden rounded-md">' +
'<img class="h-full w-full object-' + objectFit + '" src="' +
escapeForTemplate(this.config.allowHTML, imgSrc) +
'" alt=""></div>'
: ''
}
Expand All @@ -149,7 +167,7 @@ export default (asyncUrl = '') => ({
`)
},
choice: ({classNames}, data) => {
const image = data.customProperties?.image
const { src: imgSrc, width, height, objectFit } = normalizeImageData(data.customProperties?.image);

return strToEl(`
<div class="flex gap-x-2 items-center ${classNames.item} ${classNames.itemChoice} ${
Expand All @@ -165,10 +183,10 @@ export default (asyncUrl = '') => ({
}>
<div class="flex gap-x-2 items-center">
${
image
? '<div class="zoom-in h-10 w-10 overflow-hidden rounded-md">' +
'<img class="h-full w-full object-cover" src="' +
escapeForTemplate(this.config.allowHTML, image) +
imgSrc
? '<div class="zoom-in h-' + height + ' w-' + width + ' overflow-hidden rounded-md">' +
'<img class="h-full w-full object-' + objectFit + '" src="' +
escapeForTemplate(this.config.allowHTML, imgSrc) +
'" alt=""></div>'
: ''
}
Expand Down
157 changes: 157 additions & 0 deletions tests/Unit/Fields/SelectFieldTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
declare(strict_types=1);

use Illuminate\Database\Eloquent\Model;
use MoonShine\Support\DTOs\Select\Option;
use MoonShine\Support\DTOs\Select\OptionImage;
use MoonShine\Support\DTOs\Select\OptionProperty;
use MoonShine\Support\DTOs\Select\Options;
use MoonShine\Support\Enums\ObjectFit;
use MoonShine\Tests\Fixtures\Resources\TestResourceBuilder;
use MoonShine\UI\Fields\Select;

Expand Down Expand Up @@ -180,3 +184,156 @@
;
});
});

describe('select field with images (passed via arrays)', function () {
it('renders images passed as strings', function () {
$field = Select::make('Select field with images')
->options([
1 => 'Option 1',
2 => 'Option 2',
])
->optionProperties(fn() => [
1 => ['image' => 'image1.jpg'],
2 => ['image' => 'image2.png'],
]);

$result = $field->toArray();

expect($result['values'][1]['properties']['image'])->toBe([
'src' => 'image1.jpg',
'width' => 10,
'height' => 10,
'objectFit' => ObjectFit::COVER->value
])->and($result['values'][2]['properties']['image'])->toBe([
'src' => 'image2.png',
'width' => 10,
'height' => 10,
'objectFit' => ObjectFit::COVER->value
]);
});

it('renders images passed via OptionImage object', function () {
$field = Select::make('Select field with images')
->options([
1 => 'Option 1',
2 => 'Option 2',
])
->optionProperties(fn() => [
1 => ['image' => new OptionImage('image1.jpg', 6, 6, ObjectFit::FILL)],
2 => ['image' => new OptionImage('image2.png')],
]);

$result = $field->toArray();

expect($result['values'][1]['properties']['image'])->toBe([
'src' => 'image1.jpg',
'width' => 6,
'height' => 6,
'objectFit' => ObjectFit::FILL->value
])->and($result['values'][2]['properties']['image'])->toBe([
'src' => 'image2.png',
'width' => 10,
'height' => 10,
'objectFit' => ObjectFit::COVER->value
]);
});
});

describe('select field with images (passed via Options object)', function () {
it('renders images passed as strings', function () {
$options = new Options([
new Option(
'Option 1',
'1',
properties: new OptionProperty('image1.jpg')
),
new Option(
'Option 2',
'2',
true,
properties: new OptionProperty('image2.png')
)
]);

$field = Select::make('Select field with images')
->options($options);

$result = $field->toArray();

expect($result['values'][1]['properties']['image'])->toBe([
'src' => 'image1.jpg',
'width' => 10,
'height' => 10,
'objectFit' => ObjectFit::COVER->value
])->and($result['values'][2]['properties']['image'])->toBe([
'src' => 'image2.png',
'width' => 10,
'height' => 10,
'objectFit' => ObjectFit::COVER->value
]);
});

it('renders images passed via OptionImage', function () {
$options = new Options([
new Option(
'Option 1',
'1',
properties: new OptionProperty(
new OptionImage(
src: 'image1.jpg',
width: 5,
height: 5,
objectFit: ObjectFit::COVER
)
)
),
new Option(
'Option 2',
'2',
properties: new OptionProperty(
new OptionImage(
src: 'image2.png',
width: 8,
objectFit: ObjectFit::CONTAIN
)
)
)
]);

$field = Select::make('Select field with images')
->options($options)
->default('2')
->multiple();

$result = $field->toArray();

expect($result['values'][1]['properties']['image'])->toBe([
'src' => 'image1.jpg',
'width' => 5,
'height' => 5,
'objectFit' => ObjectFit::COVER->value
])->and($result['values'][2]['properties']['image'])->toBe([
'src' => 'image2.png',
'width' => 8,
'height' => 10,
'objectFit' => ObjectFit::CONTAIN->value
]);
});

it('handles empty images', function () {
$options = new Options([
new Option(
'Option 1',
'1',
properties: new OptionProperty()
)
]);

$field = Select::make('Select field with images')
->options($options);

$result = $field->toArray();

expect($result['values'][1]['properties']['image'])->toBeNull();
});
});

0 comments on commit a62ad26

Please sign in to comment.