Skip to content

Commit c6fc505

Browse files
Initial setup of per asset validation
1 parent 77174ec commit c6fc505

15 files changed

Lines changed: 251 additions & 316 deletions

File tree

resources/js/components/fieldtypes/assets/Asset.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default {
88
props: {
99
asset: Object,
1010
readOnly: Boolean,
11+
errors: Array,
1112
showFilename: {
1213
type: Boolean,
1314
default: true,

resources/js/components/fieldtypes/assets/AssetRow.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
<button
2424
v-if="showFilename"
2525
@click="editOrOpen"
26-
class="flex w-full flex-1 items-center truncate text-sm text-gray-600 dark:text-gray-400 text-start"
26+
class="flex flex-col w-full flex-1 justify-center gap-1 truncate text-sm text-gray-600 dark:text-gray-400 text-start"
2727
:title="__('Edit')"
2828
:aria-label="__('Edit Asset')"
2929
>
30-
{{ asset.basename }}
30+
<div>{{ asset.basename }}</div>
31+
<template v-if="errors.length">
32+
<small class="text-xs text-red-500" v-for="(error, i) in errors" :key="i" v-text="error" />
33+
</template>
3134
</button>
3235
<ui-badge
3336
v-if="showSetAlt && needsAlt"

resources/js/components/fieldtypes/assets/AssetTile.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020

2121
<div class="asset-thumb-container">
2222
<div class="asset-thumb" :class="{ 'bg-checkerboard': canBeTransparent }">
23+
<template v-if="errors.length">
24+
<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">
25+
<small
26+
class="text-xs text-red-500 text-center"
27+
v-text="errors[0]"
28+
/>
29+
</div>
30+
</template>
31+
2332
<!-- Solo Bard -->
2433
<template v-if="isImage && isInBardField && !isInAssetBrowser">
2534
<img :src="asset.url" />
@@ -34,7 +43,7 @@
3443
</template>
3544
</template>
3645

37-
<div class="asset-controls">
46+
<div class="asset-controls z-10">
3847
<div class="flex items-center justify-center space-x-1 rtl:space-x-reverse">
3948
<template v-if="!readOnly">
4049
<button @click="edit" class="btn btn-icon" :title="__('Edit')">

resources/js/components/fieldtypes/assets/AssetsFieldtype.vue

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@
105105
ref="assets"
106106
>
107107
<asset-tile
108-
v-for="asset in assets"
108+
v-for="(asset, index) in assets"
109109
:key="asset.id"
110110
:asset="asset"
111+
:errors="errors[index] ?? []"
111112
:read-only="isReadOnly"
112113
:show-filename="config.show_filename"
113114
:show-set-alt="showSetAlt"
@@ -134,9 +135,10 @@
134135
<component
135136
is="assetRow"
136137
class="asset-row"
137-
v-for="asset in assets"
138+
v-for="(asset, index) in assets"
138139
:key="asset.id"
139140
:asset="asset"
141+
:errors="errors[index] ?? []"
140142
:read-only="isReadOnly"
141143
:show-filename="config.show_filename"
142144
:show-set-alt="showSetAlt"
@@ -425,6 +427,27 @@ export default {
425427
},
426428
];
427429
},
430+
431+
errors() {
432+
const state = this.publishContainer;
433+
console.log('state', state);
434+
435+
if (!state) {
436+
return {};
437+
}
438+
439+
let errors = {};
440+
441+
// Filter errors to only include those for this field, and remove the field path prefix
442+
// if there is one, then append it to the errors object.
443+
Object.entries(state.errors)
444+
.filter(([key, value]) => key.startsWith(this.fieldPathPrefix || this.handle))
445+
.forEach(([key, value]) => {
446+
errors[key.split('.').pop()] = value;
447+
});
448+
449+
return errors;
450+
},
428451
},
429452
430453
events: {

resources/lang/en/validation.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,17 @@
4444
'different' => 'This field and :other must be different.',
4545
'digits' => 'Must be :digits digits.',
4646
'digits_between' => 'Must be between :min and :max digits.',
47-
'dimensions' => 'Invalid image dimensions.',
47+
'dimensions' => [
48+
'unknown' => 'Image dimensions are unknown.',
49+
'ratio' => 'Must have a ratio of :ratio.',
50+
'same' => 'Must be :comparison :width×:height pixels.',
51+
'different' => 'Must be :comparison_width :width pixels wide and :comparison_height :height pixels tall.',
52+
'width' => 'Must be :comparison_width :width pixels wide.',
53+
'height' => 'Must be :comparison_height :height pixels tall.',
54+
'min' => 'at least',
55+
'max' => 'at most',
56+
'exact' => 'exactly',
57+
],
4858
'distinct' => 'This field has a duplicate value.',
4959
'doesnt_end_with' => 'Must not end with one of the following: :values.',
5060
'doesnt_start_with' => 'Must not start with one of the following: :values.',
@@ -66,7 +76,7 @@
6676
'numeric' => 'Must be greater than or equal :value.',
6777
'string' => 'Must be greater than or equal :value characters.',
6878
],
69-
'image' => 'Must be an image.',
79+
'image' => 'Must be an image of type: :extensions.',
7080
'in' => 'This is invalid.',
7181
'in_array' => 'This field does not exist in :other.',
7282
'integer' => 'Must be an integer.',

src/Fields/Field.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,21 @@ public function alwaysSave()
137137

138138
public function rules()
139139
{
140-
$rules = [$this->handle => $this->addNullableRule(array_merge(
140+
$temp_rules = collect($this->addNullableRule(array_merge(
141141
$this->get('required') ? ['required'] : [],
142142
Validator::explodeRules($this->fieldtype()->fieldRules()),
143143
Validator::explodeRules($this->fieldtype()->rules())
144-
))];
144+
)));
145+
146+
$rules = [];
147+
if ($this->type() === 'assets') {
148+
$rules = [
149+
$this->handle.'.*' => $temp_rules->reject(fn ($rule) => in_array($rule, ['array', 'required']))->all(),
150+
$this->handle => $temp_rules->filter(fn ($rule) => in_array($rule, ['array', 'required']))->all(),
151+
];
152+
} else {
153+
$rules = [$this->handle => $temp_rules->all()];
154+
}
145155

146156
$extra = collect($this->fieldtype()->extraRules())->map(function ($rules) {
147157
return $this->addNullableRule(Validator::explodeRules($rules));

src/Fieldtypes/Assets/DimensionsRule.php

Lines changed: 84 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,132 +2,124 @@
22

33
namespace Statamic\Fieldtypes\Assets;
44

5-
use Illuminate\Contracts\Validation\Rule;
6-
use Statamic\Contracts\GraphQL\CastableToValidationString;
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
77
use Statamic\Facades\Asset;
88
use Statamic\Statamic;
99
use Symfony\Component\HttpFoundation\File\UploadedFile;
1010

11-
class DimensionsRule implements CastableToValidationString, Rule
11+
class DimensionsRule implements ValidationRule
1212
{
13-
protected $parameters;
14-
15-
public function __construct($parameters = null)
13+
public function __construct(protected $parameters)
1614
{
17-
$this->parameters = $parameters;
15+
$this->parameters = array_reduce($parameters, function ($result, $item) {
16+
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);
17+
18+
$result[$key] = $value;
19+
20+
return $result;
21+
});
1822
}
1923

20-
/**
21-
* Determine if the validation rule passes.
22-
*
23-
* @param string $attribute
24-
* @param mixed $value
25-
* @return bool
26-
*/
27-
public function passes($attribute, $value)
24+
public function validate(string $attribute, mixed $value, Closure $fail): void
2825
{
29-
return collect($value)->every(function ($id) {
30-
if ($id instanceof UploadedFile) {
31-
if (in_array($id->getMimeType(), ['image/svg+xml', 'image/svg'])) {
32-
return true;
33-
}
34-
35-
$size = getimagesize($id->getPathname());
36-
} else {
37-
if (! $asset = Asset::find($id)) {
38-
return false;
39-
}
40-
41-
if ($asset->isSvg()) {
42-
return true;
43-
}
44-
45-
$size = $asset->dimensions();
26+
$size = [0, 0];
27+
28+
if ($value instanceof UploadedFile) {
29+
if (in_array($value->getMimeType(), ['image/svg+xml', 'image/svg'])) {
30+
return;
4631
}
4732

48-
[$width, $height] = $size;
33+
$size = getimagesize($value->getPathname());
34+
} elseif ($asset = Asset::find($value)) {
35+
if ($asset->isSvg()) {
36+
return;
37+
}
4938

50-
$parameters = $this->parseNamedParameters($this->parameters);
39+
$size = $asset->dimensions();
40+
}
5141

52-
if ($this->failsBasicDimensionChecks($parameters, $width, $height) ||
53-
$this->failsRatioCheck($parameters, $width, $height)) {
54-
return false;
55-
}
42+
[$width, $height] = $size;
43+
if (! is_int($width) || ! is_int($height)) {
44+
$fail(__('statamic::validation.dimensions.unknown'));
5645

57-
return true;
58-
});
59-
}
46+
return;
47+
}
6048

61-
/**
62-
* Get the validation error message.
63-
*
64-
* @return string
65-
*/
66-
public function message()
67-
{
68-
return __((Statamic::isCpRoute() ? 'statamic::' : '').'validation.dimensions');
49+
if ($message = $this->message($width, $height)) {
50+
$fail($message);
51+
}
6952
}
7053

71-
/**
72-
* Parse named parameters to $key => $value items.
73-
*
74-
* @param array $parameters
75-
* @return array
76-
*/
77-
protected function parseNamedParameters($parameters)
54+
public function message(int $width, int $height): ?string
7855
{
79-
return array_reduce($parameters, function ($result, $item) {
80-
[$key, $value] = array_pad(explode('=', $item, 2), 2, null);
56+
$invalid_ratio = $this->validateRatio($width, $height);
57+
$invalid_width = $this->validateWidth($width);
58+
$invalid_height = $this->validateHeight($height);
59+
$key = match (true) {
60+
$invalid_ratio => 'ratio',
61+
$invalid_width && $invalid_height && $invalid_width === $invalid_height => 'same',
62+
$invalid_width && $invalid_height && $invalid_width !== $invalid_height => 'different',
63+
(bool) $invalid_width => 'width',
64+
(bool) $invalid_height => 'height',
65+
default => null,
66+
};
67+
68+
if (! $key) {
69+
return null;
70+
}
8171

82-
$result[$key] = $value;
72+
$prefix = Statamic::isCpRoute() ? 'statamic::' : '';
73+
74+
$comparisons = [
75+
'min' => __("{$prefix}validation.dimensions.min"),
76+
'max' => __("{$prefix}validation.dimensions.max"),
77+
'exact' => __("{$prefix}validation.dimensions.exact"),
78+
];
79+
80+
return __("{$prefix}validation.dimensions.{$key}", [
81+
'width' => $this->parameters['width'] ?? $this->parameters['min_width'] ?? $this->parameters['max_width'] ?? null,
82+
'height' => $this->parameters['height'] ?? $this->parameters['min_height'] ?? $this->parameters['max_height'] ?? null,
83+
'ratio' => $this->parameters['ratio'] ?? null,
84+
'comparison' => $comparisons[$invalid_width] ?? '',
85+
'comparison_width' => $comparisons[$invalid_width] ?? '',
86+
'comparison_height' => $comparisons[$invalid_height] ?? '',
87+
]);
88+
}
8389

84-
return $result;
85-
});
90+
public function validateWidth(int $width): ?string
91+
{
92+
return match (true) {
93+
isset($this->parameters['width']) && $this->parameters['width'] != $width => 'exact',
94+
isset($this->parameters['min_width']) && $this->parameters['min_width'] > $width => 'min',
95+
isset($this->parameters['max_width']) && $this->parameters['max_width'] < $width => 'max',
96+
default => null,
97+
};
8698
}
8799

88-
/**
89-
* Test if the given width and height fail any conditions.
90-
*
91-
* @param array $parameters
92-
* @param int $width
93-
* @param int $height
94-
* @return bool
95-
*/
96-
protected function failsBasicDimensionChecks($parameters, $width, $height)
100+
public function validateHeight(int $height): ?string
97101
{
98-
return (isset($parameters['width']) && $parameters['width'] != $width) ||
99-
(isset($parameters['min_width']) && $parameters['min_width'] > $width) ||
100-
(isset($parameters['max_width']) && $parameters['max_width'] < $width) ||
101-
(isset($parameters['height']) && $parameters['height'] != $height) ||
102-
(isset($parameters['min_height']) && $parameters['min_height'] > $height) ||
103-
(isset($parameters['max_height']) && $parameters['max_height'] < $height);
102+
return match (true) {
103+
isset($this->parameters['height']) && $this->parameters['height'] != $height => 'exact',
104+
isset($this->parameters['min_height']) && $this->parameters['min_height'] > $height => 'min',
105+
isset($this->parameters['max_height']) && $this->parameters['max_height'] < $height => 'max',
106+
default => null,
107+
};
104108
}
105109

106-
/**
107-
* Determine if the given parameters fail a dimension ratio check.
108-
*
109-
* @param array $parameters
110-
* @param int $width
111-
* @param int $height
112-
* @return bool
113-
*/
114-
protected function failsRatioCheck($parameters, $width, $height)
110+
public function validateRatio(int $width, int $height): bool
115111
{
116-
if (! isset($parameters['ratio'])) {
112+
if (! isset($this->parameters['ratio'])) {
117113
return false;
118114
}
119115

120116
[$numerator, $denominator] = array_replace(
121-
[1, 1], array_filter(sscanf($parameters['ratio'], '%f/%d'))
117+
[1, 1],
118+
array_filter(sscanf($this->parameters['ratio'], '%f/%d'))
122119
);
123120

124121
$precision = 1 / (max($width, $height) + 1);
125122

126123
return abs($numerator / $denominator - $width / $height) > $precision;
127124
}
128-
129-
public function toGqlValidationString(): string
130-
{
131-
return 'dimensions:'.implode(',', $this->parameters);
132-
}
133125
}

0 commit comments

Comments
 (0)