Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources/js/components/fieldtypes/assets/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
props: {
asset: Object,
readOnly: Boolean,
errors: Array,
showFilename: {
type: Boolean,
default: true,
Expand Down
7 changes: 5 additions & 2 deletions resources/js/components/fieldtypes/assets/AssetRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
<button
v-if="showFilename"
@click="editOrOpen"
class="w-full truncate text-sm text-gray-600 dark:text-gray-400 text-start"
class="flex flex-col w-full flex-1 justify-center gap-1 truncate text-sm text-gray-600 dark:text-gray-400 text-start"
:title="__('Edit')"
:aria-label="__('Edit Asset')"
>
{{ asset.basename }}
<div>{{ asset.basename }}</div>
<template v-if="errors.length">
<small class="text-xs text-red-500" v-for="(error, i) in errors" :key="i" v-text="error" />
</template>
</button>
</td>
<td class="absolute top-0 right-0 flex items-center bg-gradient-to-r to-20% from-transparent to-white dark:to-gray-800 p-3 ps-[2rem] align-middle text-end">
Expand Down
11 changes: 10 additions & 1 deletion resources/js/components/fieldtypes/assets/AssetTile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@

<div class="flex h-full border-b dark:border-gray-700 rounded-b-md relative">
<div class="p-1 flex flex-col items-center justify-center h-full" :class="{ 'bg-checkerboard': canBeTransparent }">
<template v-if="errors.length">
<div class="absolute z-10 inset-0 bg-white/75 dark:bg-dark-800/90 flex flex-col gap-2 items-center justify-center px-1 py-2">
<small
class="text-xs text-red-500 text-center"
v-text="errors[0]"
/>
</div>
</template>

<!-- Solo Bard -->
<template v-if="isImage && isInBardField && !isInAssetBrowser">
<img :src="asset.url" />
Expand All @@ -34,7 +43,7 @@
</template>
</template>
</div>
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 duration-100">
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 duration-100 z-10">
<div class="flex items-center justify-center gap-2">
<template v-if="!readOnly">
<ui-button size="sm" @click="editOrOpen" :icon="asset.isEditable ? 'pencil' : 'eye'" aria-label="__('Edit')" v-if="asset.isViewable" />
Expand Down
34 changes: 34 additions & 0 deletions resources/js/components/fieldtypes/assets/AssetsFieldtype.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:errors="errorsForAsset(asset.id)"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
Expand Down Expand Up @@ -147,6 +148,7 @@
v-for="asset in assets"
:key="asset.id"
:asset="asset"
:errors="errorsForAsset(asset.id)"
:read-only="isReadOnly"
:show-filename="config.show_filename"
:show-set-alt="showSetAlt"
Expand Down Expand Up @@ -233,6 +235,7 @@ export default {
innerDragging: false,
displayMode: 'grid',
lockedDynamicFolder: this.meta.dynamicFolder,
errorsById: {},
};
},

Expand Down Expand Up @@ -611,6 +614,14 @@ export default {
this.loadAssets([...this.value, id]);
}
},

errorsForAsset(id) {
if (Object.keys(this.errorsById).length === 0 || !this.errorsById.hasOwnProperty(id)) {
return [];
}

return this.errorsById[id];
},
},

watch: {
Expand All @@ -630,6 +641,29 @@ export default {
}
},

'publishContainer.errors': {
immediate: true,
handler(errors) {
this.errorsById = Object.entries(errors).reduce((acc, [key, value]) => {
const prefix = this.fieldPathKeys || this.handle;

if (!key.startsWith(prefix)) {
return acc;
}

const subKey = key.replace(`${prefix}.`, '');
const assetIndex = subKey.split('.').shift();
const assetId = this.assetIds[assetIndex];

if (assetId) {
acc[assetId] = value;
}

return acc;
}, {});
},
},

loading(loading) {
this.$progress.loading(`assets-fieldtype-${this.$.uid}`, loading);
},
Expand Down
22 changes: 20 additions & 2 deletions src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,29 @@ public function alwaysSave()

public function rules()
{
$rules = [$this->handle => $this->addNullableRule(array_merge(
$temp_rules = collect($this->addNullableRule(array_merge(
$this->get('required') ? ['required'] : [],
Validator::explodeRules($this->fieldtype()->fieldRules()),
Validator::explodeRules($this->fieldtype()->rules())
))];
)));

$rules = [];
if ($this->type() === 'assets') {
$rules = $temp_rules->reduce(function ($result, $rule) {
// These rules need to be applied to the field as a whole vs each asset in the field
if (Str::of($rule)->before(':')->is(['array', 'required', 'nullable', 'max', 'min'])) {
$result[$this->handle] ??= [];
$result[$this->handle][] = $rule;
} else {
$result["{$this->handle}.*"] ??= [];
$result["{$this->handle}.*"][] = $rule;
}

return $result;
}, []);
} else {
$rules = [$this->handle => $temp_rules->all()];
}

$extra = collect($this->fieldtype()->extraRules())->map(function ($rules) {
return $this->addNullableRule(Validator::explodeRules($rules));
Expand Down
100 changes: 38 additions & 62 deletions src/Fieldtypes/Assets/DimensionsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,60 @@

namespace Statamic\Fieldtypes\Assets;

use Illuminate\Contracts\Validation\Rule;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class DimensionsRule implements CastableToValidationString, Rule
class DimensionsRule implements CastableToValidationString, Stringable, ValidationRule
{
protected $parameters;
protected array $raw_parameters;

public function __construct($parameters = null)
public function __construct(protected $parameters)
{
$this->parameters = $parameters;
$this->raw_parameters = $parameters;
$this->parameters = array_reduce($parameters, function ($acc, $item) {
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);
$acc[$key] = $value;

return $acc;
}, []);
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
public function validate(string $attribute, mixed $value, Closure $fail): void
{
return collect($value)->every(function ($id) {
if ($id instanceof UploadedFile) {
if (in_array($id->getMimeType(), ['image/svg+xml', 'image/svg'])) {
return true;
}

$size = getimagesize($id->getPathname());
} else {
if (! $asset = Asset::find($id)) {
return false;
}

if ($asset->isSvg()) {
return true;
}

$size = $asset->dimensions();
$size = [0, 0];

if ($value instanceof UploadedFile) {
if (in_array($value->getMimeType(), ['image/svg+xml', 'image/svg'])) {
return;
}

[$width, $height] = $size;
$size = getimagesize($value->getPathname());
} elseif ($asset = Asset::find($value)) {
if ($asset->isSvg()) {
return;
}

$parameters = $this->parseNamedParameters($this->parameters);
$size = $asset->dimensions();
}

if ($this->failsBasicDimensionChecks($parameters, $width, $height) ||
$this->failsRatioCheck($parameters, $width, $height)) {
return false;
}
[$width, $height] = $size;

return true;
});
if ($this->failsBasicDimensionChecks($this->parameters, $width, $height) ||
$this->failsRatioCheck($this->parameters, $width, $height)) {
$fail($this->message());
}
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
public function message(): string
{
return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.dimensions');
}

/**
* Parse named parameters to $key => $value items.
*
* @param array $parameters
* @return array
*/
protected function parseNamedParameters($parameters)
{
return array_reduce($parameters, function ($result, $item) {
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);

$result[$key] = $value;

return $result;
});
}

/**
* Test if the given width and height fail any conditions.
*
Expand Down Expand Up @@ -126,8 +97,13 @@ protected function failsRatioCheck($parameters, $width, $height)
return abs($numerator / $denominator - $width / $height) > $precision;
}

public function __toString()
{
return 'dimensions:'.implode(',', $this->raw_parameters);
}

public function toGqlValidationString(): string
{
return 'dimensions:'.implode(',', $this->parameters);
return $this->__toString();
}
}
57 changes: 26 additions & 31 deletions src/Fieldtypes/Assets/ImageRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,52 @@

namespace Statamic\Fieldtypes\Assets;

use Illuminate\Contracts\Validation\Rule;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Statamic\Contracts\GraphQL\CastableToValidationString;
use Statamic\Facades\Asset;
use Statamic\Statamic;
use Stringable;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class ImageRule implements CastableToValidationString, Rule
class ImageRule implements CastableToValidationString, Stringable, ValidationRule
{
protected $parameters;
public $extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'avif'];

public function __construct($parameters = null)
public function __construct(protected $parameters)
{
$this->parameters = $parameters;
if ($this->parameters !== ['image']) {
$this->extensions = array_map(strtolower(...), $this->parameters);
}
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'avif'];
$extension = '';

return collect($value)->every(function ($id) use ($extensions) {
if ($id instanceof UploadedFile) {
return in_array($id->guessExtension(), $extensions);
}
if ($value instanceof UploadedFile) {
$extension = $value->guessExtension();
} elseif ($asset = Asset::find($value)) {
$extension = $asset->extension();
}

if (! $asset = Asset::find($id)) {
return false;
}
if (! in_array($extension, $this->extensions)) {
$fail($this->message());
}
}

return $asset->guessedExtensionIsOneOf($extensions);
});
public function message(): string
{
return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image', ['extensions' => implode(', ', $this->extensions)]);
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
public function __toString()
{
return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.image');
return 'image:'.implode(',', $this->extensions);
}

public function toGqlValidationString(): string
{
return 'image:'.implode(',', $this->parameters);
return $this->__toString();
}
}
Loading
Loading