{
+ id: number;
+ uid: string;
+ handle: string;
+ name: string;
+}
diff --git a/resources/public/images/transforms/crop.svg b/resources/public/images/transforms/crop.svg
new file mode 100644
index 00000000000..d2cb2d0b703
--- /dev/null
+++ b/resources/public/images/transforms/crop.svg
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file
diff --git a/resources/public/images/transforms/fit.svg b/resources/public/images/transforms/fit.svg
new file mode 100644
index 00000000000..6efbcf45d81
--- /dev/null
+++ b/resources/public/images/transforms/fit.svg
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file
diff --git a/resources/public/images/transforms/letterbox.svg b/resources/public/images/transforms/letterbox.svg
new file mode 100644
index 00000000000..2abefaf7fdb
--- /dev/null
+++ b/resources/public/images/transforms/letterbox.svg
@@ -0,0 +1,36 @@
+
+
\ No newline at end of file
diff --git a/resources/public/images/transforms/stretch.svg b/resources/public/images/transforms/stretch.svg
new file mode 100644
index 00000000000..fcb21cf0be6
--- /dev/null
+++ b/resources/public/images/transforms/stretch.svg
@@ -0,0 +1,37 @@
+
+
\ No newline at end of file
diff --git a/resources/templates/settings/assets/transforms/_index.twig b/resources/templates/settings/assets/transforms/_index.twig
deleted file mode 100644
index 9b286632a35..00000000000
--- a/resources/templates/settings/assets/transforms/_index.twig
+++ /dev/null
@@ -1,62 +0,0 @@
-{% extends "settings/assets/_layout" %}
-{% set selectedNavItem = 'transforms' %}
-{% set readOnly = readOnly ?? false %}
-
-{% do registerLegacyAsset('CraftCms\\Cms\\View\\LegacyAssets\\AdminTableAsset') -%}
-
-
-{% if readOnly %}
- {% set contentNotice = readOnlyNotice() %}
-{% endif %}
-
-{% block content %}
-
-
- {% if not readOnly %}
-
- {% endif %}
-{% endblock %}
-
-{% set tableData = [] %}
-{% for transform in transforms %}
- {% if transform.mode %}
- {% set mode = modes[transform.mode] %}
- {% endif %}
-
- {% set tableData = tableData|merge([{
- id: transform.id,
- title: transform.name|t('site'),
- url: url('settings/assets/transforms/' ~ transform.handle),
- handle: transform.handle,
- mode: mode ?? null,
- dimensions: (transform.width ? transform.width : 'Auto'|t('app')|e) ~ " × " ~ (transform.height ? transform.height : 'Auto'|t('app')|e),
- interlace: transform.interlace ? transform.interlace|capitalize : 'None'|t('app')|e,
- format: transform.format ? transform.format|capitalize : 'Auto'|t('app')|e,
- }]) %}
-{% endfor %}
-
-{% js %}
-var columns = [
- { name: '__slot:title', title: Craft.t('app', 'Name') },
- { name: '__slot:handle', title: Craft.t('app', 'Handle') },
- { name: 'mode', title: Craft.t('app', 'Mode'), },
- { name: 'dimensions', title: Craft.t('app', 'Dimensions'), },
- { name: 'interlace', title: Craft.t('app', 'Interlace'), },
- { name: 'format', title: Craft.t('app', 'Format'), }
-];
-
-let config = {
- columns: columns,
- container: '#transforms-vue-admin-table',
- emptyMessage: Craft.t('app', 'No image transforms exist yet.'),
- tableData: {{ tableData|json_encode|raw }},
-};
-
-{% if not readOnly %}
- config['deleteAction'] = 'image-transforms/delete';
-{% endif %}
-
-new Craft.VueAdminTable(config);
-{% endjs %}
diff --git a/resources/templates/settings/assets/transforms/_settings.twig b/resources/templates/settings/assets/transforms/_settings.twig
deleted file mode 100644
index 1a95fdb257b..00000000000
--- a/resources/templates/settings/assets/transforms/_settings.twig
+++ /dev/null
@@ -1,319 +0,0 @@
-{% extends '_layouts/cp.twig' %}
-
-{% set readOnly = readOnly ?? false %}
-{% set fullPageForm = not readOnly %}
-
-{% set crumbs = [
- { label: "Settings"|t('app'), url: url('settings') },
- { label: "Assets"|t('app'), url: url('settings/assets') },
- { label: "Image Transforms"|t('app'), url: url('settings/assets/transforms') }
-] %}
-
-{% set formActions = [
- {
- label: 'Save and continue editing'|t('app'),
- redirect: 'settings/assets/transforms/{handle}'|hash,
- shortcut: true,
- retainScroll: true,
- },
-] %}
-
-{% set defaultPositionLabel = "Default Focal Point"|t('app') %}
-{% set letterboxPositionLabel = "Image Position"|t('app') %}
-{% set mode = old('mode', transform.mode) %}
-{% set fill = old('fill', transform.fill) %}
-{% set quality = old('quality', transform.quality) %}
-{% set format = old('format', transform.format) %}
-{% set qualityPickerValue = 0 %}
-{% if quality %}
- {% set matchingQualityOption = qualityPickerOptions|filter(o => quality >= o.value)|last %}
- {% set qualityPickerValue = matchingQualityOption ? matchingQualityOption.value : 10 %}
-{% endif %}
-
-{% import '_includes/forms.twig' as forms %}
-
-{% if readOnly %}
- {% set contentNotice = readOnlyNotice() %}
-{% endif %}
-
-{% block content %}
- {% if not readOnly %}
- {{ actionInput('image-transforms/save') }}
- {{ redirectInput('settings/assets/transforms') }}
-
- {% if transform.id %}{{ hiddenInput('transformId', transform.id) }}{% endif %}
- {% endif %}
-
- {{ forms.textField({
- first: true,
- label: "Name"|t('app'),
- id: 'name',
- name: 'name',
- value: old('name', transform.name),
- errors: transform.errors.get('name'),
- autofocus: true,
- required: true,
- disabled: readOnly,
- }) }}
-
- {{ forms.textField({
- label: "Handle"|t('app'),
- id: "handle",
- name: "handle",
- class: 'code',
- autocorrect: false,
- autocapitalize: false,
- value: old('handle', transform.handle),
- errors: transform.errors.get('handle'),
- required: true,
- disabled: readOnly,
- }) }}
-
- {% embed '_includes/forms/field.twig' with {
- label: 'Mode'|t('app')
- } %}
- {% block input %}
-
-
-
-
-
-
- {% endblock %}
- {% endembed %}
-
-
- {{ forms.colorField({
- label: 'Fill Color',
- name: 'fill',
- value: mode == 'letterbox' and fill != 'transparent' ? fill,
- errors: transform.errors.get('fill'),
- disabled: readOnly,
- }) }}
-
-
-
- {% set positionLabel = mode == 'letterbox' ? letterboxPositionLabel : defaultPositionLabel %}
- {{ forms.selectField({
- label: positionLabel,
- id: 'position',
- name: 'position',
- options: {
- 'top-left': "Top-Left"|t('app'),
- 'top-center': "Top-Center"|t('app'),
- 'top-right': "Top-Right"|t('app'),
- 'center-left': "Center-Left"|t('app'),
- 'center-center': "Center-Center"|t('app'),
- 'center-right': "Center-Right"|t('app'),
- 'bottom-left': "Bottom-Left"|t('app'),
- 'bottom-center': "Bottom-Center"|t('app'),
- 'bottom-right': "Bottom-Right"|t('app')
- },
- value: mode in ['crop', 'letterbox'] ? old('position', transform.position) : 'center-center',
- disabled: readOnly,
- }) }}
-
-
- {{ forms.textField({
- label: "Width"|t('app'),
- id: "width",
- name: "width",
- size: 5,
- value: old('width', transform.width),
- errors: transform.errors.get('width'),
- disabled: readOnly,
- }) }}
-
- {{ forms.textField({
- label: "Height"|t('app'),
- id: "height",
- name: "height",
- size: 5,
- value: old('height', transform.height),
- errors: transform.errors.get('height'),
- disabled: readOnly,
- }) }}
-
- {{ forms.lightswitchField({
- label: 'Allow Upscaling'|t('app'),
- id: 'upscale',
- name: 'upscale',
- on: old('upscale', transform.upscale ?? config('craft.general.upscaleImages')),
- errors: transform.errors.get('upscale'),
- disabled: readOnly,
- }) }}
-
- {% embed '_includes/forms/field.twig' with {
- label: 'Quality'|t('app'),
- errors: transform.errors.get('quality'),
- } %}
- {% block input %}
- {% import '_includes/forms.twig' as forms %}
-
-
- {{ forms.select({
- id: 'quality-picker',
- options: [
- {label: 'Auto'|t('app'), value: 0},
- ]|merge(qualityPickerOptions),
- value: qualityPickerValue,
- describedBy: describedBy,
- disabled: readOnly,
- }) }}
-
-
- {{ forms.text({
- id: 'quality',
- class: [
- 'ltr',
- quality == 0 ? 'hidden',
- ]|filter,
- name: 'quality',
- value: quality,
- size: 5,
- type: 'number',
- min: 1,
- max: 100,
- describedBy: describedBy,
- disabled: readOnly,
- }) }}
-
-
- {% endblock %}
- {% endembed %}
-
- {{ forms.selectField({
- label: "Interlacing"|t('app'),
- id: "interlace",
- name: "interlace",
- options: [
- {label: 'None'|t('app'), value: 'none'},
- {label: 'Line'|t('app'), value: 'line'},
- {label: 'Plane'|t('app'), value: 'plane'},
- {label: 'Partition'|t('app'), value: 'partition'},
- ],
- value: old('interlace', transform.interlace ?? 'none'),
- errors: transform.errors.get('interlace'),
- disabled: readOnly,
- }) }}
-
- {% set formatOptions = [
- {label: 'Auto', value: null},
- {label: 'jpg', value: 'jpg'},
- {label: 'png', value: 'png'},
- {label: 'gif', value: 'gif'},
- ] %}
-
- {% if format == 'webp' or Images.supportsWebP %}
- {% set formatOptions = formatOptions|merge([{label: 'webp', value: 'webp'}]) %}
- {% endif %}
-
- {% if format == 'avif' or Images.supportsAvif %}
- {% set formatOptions = formatOptions|merge([{label: 'avif', value: 'avif'}]) %}
- {% endif %}
-
- {{ forms.selectField({
- label: "Image Format"|t('app'),
- id: "format",
- name: "format",
- instructions: "The image format that transformed images should use."|t('app'),
- value: format,
- errors: transform.errors.get('format'),
- options: formatOptions,
- disabled: readOnly,
- }) }}
-
-{% endblock %}
-
-
-{% js %}
- {% if not old('handle', transform.handle) %}new Craft.HandleGenerator('#name', '#handle');{% endif %}
-
- $('#mode input').change(function() {
- const value = $(this).val();
-
- // Letterbox mode requires a position:
- if (value == 'letterbox') {
- $('#fill-color').removeClass('hidden');
- $('#position-container label[for="position"]').text(`{{ letterboxPositionLabel }}`);
- } else {
- $('#fill-color').addClass('hidden');
- $('#position-container label[for="position"]').text(`{{ defaultPositionLabel }}`);
- }
-
- // Crop and letterbox modes requires a position:
- if (value == 'crop' || value == 'letterbox') {
- $('#position-container').removeClass('hidden');
- } else {
- $('#position-container').addClass('hidden');
- }
- });
-
- const qualityPickerOptions = {{ qualityPickerOptions|map(o => o.value)|json_encode|raw }};
- const $qualityPicker = $('#quality-picker');
- const $qualityInput = $('#quality');
-
- $qualityPicker.on('change', (ev) => {
- const pickerValue = $qualityPicker.val();
- $qualityInput.val(pickerValue);
-
- if (pickerValue === '0') {
- $qualityInput.addClass('hidden');
- } else {
- $qualityInput.removeClass('hidden');
- }
- });
-
- $qualityInput.on('input', (ev) => {
- let quality = parseInt($qualityInput.val());
- if (isNaN(quality)) {
- quality = 0;
- }
-
- let pickerValue;
- if (quality) {
- // Default to Low, even if quality is < 10
- pickerValue = 10;
- for (let i = 0; i < qualityPickerOptions.length; i++) {
- if (quality >= qualityPickerOptions[i]) {
- pickerValue = qualityPickerOptions[i];
- } else {
- break;
- }
- }
- } else {
- // Auto
- pickerValue = 0;
- }
-
- $qualityPicker.val(pickerValue);
- });
-{% endjs %}
diff --git a/routes/actions.php b/routes/actions.php
index ef73adc8619..743b6123bfd 100644
--- a/routes/actions.php
+++ b/routes/actions.php
@@ -72,7 +72,6 @@
use CraftCms\Cms\Http\Controllers\RelationalFieldsController;
use CraftCms\Cms\Http\Controllers\Settings\EntryTypesController;
use CraftCms\Cms\Http\Controllers\Settings\FilesystemsController;
-use CraftCms\Cms\Http\Controllers\Settings\ImageTransformsController;
use CraftCms\Cms\Http\Controllers\Settings\RoutesController;
use CraftCms\Cms\Http\Controllers\Settings\SectionsController;
use CraftCms\Cms\Http\Controllers\Settings\UserSettingsController;
@@ -428,7 +427,6 @@
Route::middleware([RequireAdminChanges::class])->group(function () {
Route::post('volumes/save-volume', [VolumesController::class, 'save']);
Route::post('volumes/reorder-volumes', [VolumesController::class, 'reorder']);
- Route::post('image-transforms/save', [ImageTransformsController::class, 'save']);
});
// Plugins
diff --git a/routes/cp.php b/routes/cp.php
index 41ac234a0c9..54231959254 100644
--- a/routes/cp.php
+++ b/routes/cp.php
@@ -251,10 +251,19 @@
Route::middleware(RequireAdminChanges::class)->get('settings/assets/volumes/new', [VolumesController::class, 'create']);
Route::get('settings/assets/volumes/{volumeId}', [VolumesController::class, 'edit'])->whereNumber('volumeId');
Route::middleware(RequireAdminChanges::class)->delete('settings/assets/volumes/{volumeId}', [VolumesController::class, 'destroy'])->whereNumber('volumeId');
- Route::get('settings/assets/transforms', [ImageTransformsController::class, 'index']);
- Route::middleware(RequireAdminChanges::class)->get('settings/assets/transforms/new', [ImageTransformsController::class, 'create']);
- Route::get('settings/assets/transforms/{transformHandle}', [ImageTransformsController::class, 'edit']);
- Route::middleware(RequireAdminChanges::class)->delete('settings/assets/transforms/{transformId}', [ImageTransformsController::class, 'destroy']);
+
+ // Transforms
+ Route::prefix('settings/assets/transforms')->name('settings.assets.transforms.')->group(function () {
+ Route::get('/', [ImageTransformsController::class, 'index'])->name('index');
+
+ Route::middleware(RequireAdminChanges::class)->group(function () {
+ Route::get('new', [ImageTransformsController::class, 'create'])->name('create');
+ Route::post('/', [ImageTransformsController::class, 'store']);
+ Route::delete('{transformId}', [ImageTransformsController::class, 'destroy'])->name('destroy');
+ });
+
+ Route::get('{transformHandle}', [ImageTransformsController::class, 'edit'])->name('edit');
+ });
// Sites
Route::get('settings/sites', [SitesController::class, 'index'])
diff --git a/src/Http/Controllers/Settings/ImageTransformsController.php b/src/Http/Controllers/Settings/ImageTransformsController.php
index dd4bb4123c6..aa1fa63342a 100644
--- a/src/Http/Controllers/Settings/ImageTransformsController.php
+++ b/src/Http/Controllers/Settings/ImageTransformsController.php
@@ -4,33 +4,24 @@
namespace CraftCms\Cms\Http\Controllers\Settings;
-use CraftCms\Cms\Config\GeneralConfig;
use CraftCms\Cms\Http\RespondsWithFlash;
+use CraftCms\Cms\Http\Responses\CpScreenResponse;
use CraftCms\Cms\Image\Data\ImageTransform;
+use CraftCms\Cms\Image\Images;
use CraftCms\Cms\Image\ImageTransforms;
use CraftCms\Cms\Support\Url;
use CraftCms\Cms\Validation\Rules\ColorRule;
-use CraftCms\Cms\View\LegacyAssets\EditTransformAsset;
-use CraftCms\Cms\View\LegacyAssets\InternalAssetRegistry;
-use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
+use Imagine\Image\Format;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
-use function CraftCms\Cms\craftAsset;
use function CraftCms\Cms\t;
class ImageTransformsController
{
use RespondsWithFlash;
- private bool $readOnly;
-
- public function __construct(GeneralConfig $generalConfig)
- {
- $this->readOnly = ! $generalConfig->allowAdminChanges;
- }
-
public function index(ImageTransforms $imageTransforms)
{
$transforms = $imageTransforms
@@ -38,10 +29,11 @@ public function index(ImageTransforms $imageTransforms)
->sort(fn (ImageTransform $a, ImageTransform $b): int => t($a->name, category: 'site') <=> t($b->name, category: 'site'))
->values();
- return Inertia::render('SettingsImageTransformsIndexPage', [
+ return Inertia::render('settings/assets/transforms/ImageTransformsIndexPage', [
'crumbs' => fn () => [
['label' => t('Settings'), 'url' => Url::cpUrl('settings')],
- ['label' => t('Transforms')],
+ ['label' => t('Assets'), 'url' => Url::cpUrl('settings/assets/transforms')],
+ ['label' => t('Image Transforms')],
],
'title' => t('Image Transforms'),
'transforms' => $transforms,
@@ -49,22 +41,21 @@ public function index(ImageTransforms $imageTransforms)
]);
}
- public function create(): View
+ public function create(Images $images): CpScreenResponse
{
- abort_if($this->readOnly, 403, 'Administrative changes are disallowed in this environment.');
-
- return $this->editView();
+ return $this->editScreen(new ImageTransform, $images);
}
- public function edit(ImageTransforms $imageTransforms, string $transformHandle): View
+ public function edit(ImageTransforms $imageTransforms, Images $images, string $transformHandle): CpScreenResponse
{
$transform = $imageTransforms->getTransformByHandle($transformHandle);
+
abort_if(is_null($transform), 404, 'Transform not found');
- return $this->editView($transformHandle, $transform);
+ return $this->editScreen($transform, $images);
}
- public function save(Request $request, ImageTransforms $imageTransforms): Response
+ public function store(Request $request, ImageTransforms $imageTransforms): Response
{
$transform = new ImageTransform;
$transform->id = $request->integer('transformId') ?: null;
@@ -103,66 +94,147 @@ public function save(Request $request, ImageTransforms $imageTransforms): Respon
return $this->asModelFailure($transform, modelName: 'transform');
}
- return $this->asModelSuccess($transform, t('Transform saved.'), 'transform');
+ return $this->asModelSuccess(
+ $transform,
+ t('Transform saved.'),
+ 'transform',
+ redirect: $this->getPostedRedirectUrl($transform)
+ ?? Url::cpUrl("settings/assets/transforms/$transform->handle"),
+ );
}
- public function destroy(Request $request, ImageTransforms $imageTransforms, int $transformId): Response
+ public function destroy(ImageTransforms $imageTransforms, int $transformId): Response
{
$imageTransforms->deleteTransformById($transformId);
return $this->asSuccess();
}
- private function editView(?string $transformHandle = null, ?ImageTransform $transform = null): View
+ private function editScreen(ImageTransform $transform, Images $images): CpScreenResponse
{
- $transform ??= new ImageTransform;
- app(InternalAssetRegistry::class)->register(EditTransformAsset::class);
-
$title = $transform->id
? (trim((string) $transform->name) ?: t('Edit Image Transform'))
: t('Create a new image transform');
- [$qualityPickerOptions, $qualityPickerValue] = $this->qualityPickerData($transform);
+ return new CpScreenResponse()
+ ->title($title)
+ ->addCrumb(t('Settings'), 'settings')
+ ->addCrumb(t('Assets'), 'settings/assets/transforms')
+ ->addCrumb(t('Image Transforms'), 'settings/assets/transforms')
+ ->addCrumb($title)
+ ->redirectUrl('settings/assets/transforms')
+ ->inertiaPage('settings/assets/transforms/EditImageTransformPage', [
+ 'transform' => $this->transformData($transform),
+ 'modeOptions' => $this->modeOptions(),
+ 'positionOptions' => $this->positionOptions(),
+ 'interlaceOptions' => $this->interlaceOptions(),
+ 'formatOptions' => $this->formatOptions($images, $transform),
+ 'qualityOptions' => $this->qualityOptions(),
+ ]);
+ }
- return view('settings/assets/transforms/_settings', [
- 'handle' => $transformHandle,
- 'transform' => $transform,
- 'title' => $title,
- 'qualityPickerOptions' => $qualityPickerOptions,
- 'qualityPickerValue' => $qualityPickerValue,
- 'readOnly' => $this->readOnly,
- 'baseIconsUrl' => craftAsset('legacy/edittransform/dist/images'),
- ]);
+ /**
+ * @return array
+ */
+ private function transformData(ImageTransform $transform): array
+ {
+ return [
+ 'id' => $transform->id,
+ 'name' => $transform->name,
+ 'handle' => $transform->handle,
+ 'width' => $transform->width,
+ 'height' => $transform->height,
+ 'mode' => $transform->mode,
+ 'position' => $transform->position,
+ 'quality' => $transform->quality,
+ 'interlace' => $transform->interlace,
+ 'format' => $transform->format,
+ 'fill' => $transform->fill,
+ 'upscale' => $transform->upscale,
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function modeOptions(): array
+ {
+ $modes = ImageTransform::modes();
+
+ return collect(['crop', 'fit', 'letterbox', 'stretch'])
+ ->map(fn (string $value): array => [
+ 'label' => $modes[$value],
+ 'value' => $value,
+ ])
+ ->values()
+ ->all();
+ }
+
+ /**
+ * @return array
+ */
+ private function positionOptions(): array
+ {
+ return [
+ ['label' => t('Top-Left'), 'value' => 'top-left'],
+ ['label' => t('Top-Center'), 'value' => 'top-center'],
+ ['label' => t('Top-Right'), 'value' => 'top-right'],
+ ['label' => t('Center-Left'), 'value' => 'center-left'],
+ ['label' => t('Center-Center'), 'value' => 'center-center'],
+ ['label' => t('Center-Right'), 'value' => 'center-right'],
+ ['label' => t('Bottom-Left'), 'value' => 'bottom-left'],
+ ['label' => t('Bottom-Center'), 'value' => 'bottom-center'],
+ ['label' => t('Bottom-Right'), 'value' => 'bottom-right'],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function interlaceOptions(): array
+ {
+ return [
+ ['label' => t('None'), 'value' => 'none'],
+ ['label' => t('Line'), 'value' => 'line'],
+ ['label' => t('Plane'), 'value' => 'plane'],
+ ['label' => t('Partition'), 'value' => 'partition'],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private function formatOptions(Images $images, ImageTransform $transform): array
+ {
+ $options = [
+ ['label' => t('Auto'), 'value' => ''],
+ ['label' => 'jpg', 'value' => 'jpg'],
+ ['label' => 'png', 'value' => 'png'],
+ ['label' => 'gif', 'value' => 'gif'],
+ ];
+
+ if ($transform->format === Format::ID_WEBP || $images->getSupportsWebP()) {
+ $options[] = ['label' => Format::ID_WEBP, 'value' => Format::ID_WEBP];
+ }
+
+ if ($transform->format === Format::ID_AVIF || $images->getSupportsAvif()) {
+ $options[] = ['label' => Format::ID_AVIF, 'value' => Format::ID_AVIF];
+ }
+
+ return $options;
}
/**
- * @return array{0: array, 1: int}
+ * @return array
*/
- private function qualityPickerData(ImageTransform $transform): array
+ private function qualityOptions(): array
{
- $qualityPickerOptions = [
+ return [
['label' => t('Low'), 'value' => 10],
['label' => t('Medium'), 'value' => 30],
['label' => t('High'), 'value' => 60],
['label' => t('Very High'), 'value' => 80],
['label' => t('Maximum'), 'value' => 100],
];
-
- if ($transform->quality) {
- // Default to Low, even if quality is < 10.
- $qualityPickerValue = 10;
- foreach ($qualityPickerOptions as $option) {
- if ($transform->quality >= $option['value']) {
- $qualityPickerValue = $option['value'];
- } else {
- break;
- }
- }
- } else {
- // Auto
- $qualityPickerValue = 0;
- }
-
- return [$qualityPickerOptions, $qualityPickerValue];
}
}
diff --git a/src/Http/Controllers/Settings/VolumesController.php b/src/Http/Controllers/Settings/VolumesController.php
index ff5da91ca62..6915d0cfc54 100644
--- a/src/Http/Controllers/Settings/VolumesController.php
+++ b/src/Http/Controllers/Settings/VolumesController.php
@@ -55,10 +55,11 @@ public function index(Request $request, Volumes $volumes)
return Inertia::render('SettingsVolumesIndexPage', [
'crumbs' => fn () => [
['label' => t('Settings'), 'url' => Url::cpUrl('settings')],
- ['label' => t('Assets')],
+ ['label' => t('Assets'), 'url' => Url::cpUrl('settings/assets')],
+ ['label' => t('Volumes')],
],
'sort' => $sort,
- 'title' => t('Asset Settings'),
+ 'title' => t('Volume Settings'),
'volumes' => $volumes->getAllVolumes(...),
]);
}
diff --git a/src/Image/Data/ImageTransform.php b/src/Image/Data/ImageTransform.php
index 0d888a0fcce..0f066fa5fb6 100644
--- a/src/Image/Data/ImageTransform.php
+++ b/src/Image/Data/ImageTransform.php
@@ -5,6 +5,7 @@
namespace CraftCms\Cms\Image\Data;
use CraftCms\Cms\Component\Component;
+use CraftCms\Cms\Database\Table;
use CraftCms\Cms\Image\Contracts\ImageTransformerInterface;
use CraftCms\Cms\Image\ImageTransformer;
use DateTime;
@@ -141,7 +142,7 @@ public function getRules(): array
{
return [
'name' => ['required', 'string'],
- 'handle' => ['required', 'string'],
+ 'handle' => ['required', 'string', Rule::unique(Table::IMAGETRANSFORMS, 'handle')->ignore($this->id)],
'width' => ['nullable', 'integer', 'min:1'],
'height' => ['nullable', 'integer', 'min:1'],
'mode' => ['required', Rule::in(self::MODES)],
diff --git a/src/View/LegacyAssets/EditTransformAsset.php b/src/View/LegacyAssets/EditTransformAsset.php
deleted file mode 100644
index d76e0ba1f4f..00000000000
--- a/src/View/LegacyAssets/EditTransformAsset.php
+++ /dev/null
@@ -1,26 +0,0 @@
-cssFile(craftAsset('legacy/edittransform/dist/css/transforms.css'));
- }
-}
diff --git a/tests/Feature/Http/Controllers/Settings/ImageTransformsControllerTest.php b/tests/Feature/Http/Controllers/Settings/ImageTransformsControllerTest.php
index a7c074c20ab..fc3c2f4c1d8 100644
--- a/tests/Feature/Http/Controllers/Settings/ImageTransformsControllerTest.php
+++ b/tests/Feature/Http/Controllers/Settings/ImageTransformsControllerTest.php
@@ -7,14 +7,17 @@
use CraftCms\Cms\Image\Data\ImageTransform as ImageTransformData;
use CraftCms\Cms\Image\ImageTransforms;
use CraftCms\Cms\Image\Models\ImageTransform as ImageTransformModel;
+use CraftCms\Cms\Support\Url;
use CraftCms\Cms\User\Elements\User;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Crypt;
use Inertia\Testing\AssertableInertia;
use function CraftCms\Cms\t;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\deleteJson;
use function Pest\Laravel\get;
+use function Pest\Laravel\post;
use function Pest\Laravel\postJson;
beforeEach(function () {
@@ -69,7 +72,7 @@ function validTransformData(array $overrides = []): array
get(action([ImageTransformsController::class, 'index']))->assertRedirect();
get(action([ImageTransformsController::class, 'create']))->assertRedirect();
get(action([ImageTransformsController::class, 'edit'], ['transformHandle' => $transform->handle]))->assertRedirect();
- postJson(action([ImageTransformsController::class, 'save']))->assertUnauthorized();
+ postJson(action([ImageTransformsController::class, 'store']))->assertUnauthorized();
deleteJson(action([ImageTransformsController::class, 'destroy'], [$transform->id]))->assertUnauthorized();
});
@@ -80,31 +83,47 @@ function validTransformData(array $overrides = []): array
get(action([ImageTransformsController::class, 'index']))
->assertInertia(fn (AssertableInertia $page) => $page->where('readOnly', true));
get(action([ImageTransformsController::class, 'edit'], ['transformHandle' => $transform->handle]))
- ->assertOk()
- ->assertSee(t("Changes to these settings aren\u{2019}t permitted in this environment."));
+ ->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('settings/assets/transforms/EditImageTransformPage')
+ ->where('readOnly', true));
get(action([ImageTransformsController::class, 'create']))->assertForbidden();
- postJson(action([ImageTransformsController::class, 'save']), validTransformData())->assertForbidden();
+ postJson(action([ImageTransformsController::class, 'store']), validTransformData())->assertForbidden();
deleteJson(action([ImageTransformsController::class, 'destroy'], [$transform->id]))->assertForbidden();
});
it('renders index', function () {
get(action([ImageTransformsController::class, 'index']))
- ->assertInertia(fn (AssertableInertia $page) => $page->component('SettingsImageTransformsIndexPage'));
+ ->assertInertia(fn (AssertableInertia $page) => $page->component('settings/assets/transforms/ImageTransformsIndexPage'));
});
it('renders create', function () {
get(action([ImageTransformsController::class, 'create']))
- ->assertOk()
- ->assertSee(t('Create a new image transform'));
+ ->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('settings/assets/transforms/EditImageTransformPage')
+ ->where('title', t('Create a new image transform'))
+ ->where('transform.id', null)
+ ->has('modeOptions', 4)
+ ->where('modeOptions.0.value', 'crop')
+ ->where('modeOptions.1.value', 'fit')
+ ->where('modeOptions.2.value', 'letterbox')
+ ->where('modeOptions.3.value', 'stretch')
+ ->has('positionOptions', 9)
+ ->has('interlaceOptions', 4)
+ ->has('formatOptions')
+ ->has('qualityOptions', 5));
});
it('renders edit for an existing transform', function () {
$transform = createTestTransform();
get(action([ImageTransformsController::class, 'edit'], ['transformHandle' => $transform->handle]))
- ->assertOk()
- ->assertSee($transform->name);
+ ->assertInertia(fn (AssertableInertia $page) => $page
+ ->component('settings/assets/transforms/EditImageTransformPage')
+ ->where('title', $transform->name)
+ ->where('transform.id', $transform->id)
+ ->where('transform.name', $transform->name)
+ ->where('transform.handle', $transform->handle));
});
it('returns 404 for a missing transform handle', function () {
@@ -117,7 +136,7 @@ function validTransformData(array $overrides = []): array
$payload = validTransformData();
- postJson(action([ImageTransformsController::class, 'save']), $payload)
+ postJson(action([ImageTransformsController::class, 'store']), $payload)
->assertOk()
->assertJsonPath('modelName', 'transform');
@@ -131,6 +150,27 @@ function validTransformData(array $overrides = []): array
->and($transform->name)->toBe($payload['name']);
});
+it('redirects to the saved transform edit page when saving and continuing', function () {
+ $payload = validTransformData([
+ 'handle' => 'continuedTransform',
+ ]);
+
+ post(action([ImageTransformsController::class, 'store']), $payload)
+ ->assertRedirect(Url::cpUrl('settings/assets/transforms/continuedTransform'))
+ ->assertSessionHas('success', t('Transform saved.'));
+});
+
+it('redirects to the posted redirect when saving normally', function () {
+ $payload = validTransformData([
+ 'handle' => 'normallySavedTransform',
+ 'redirect' => Crypt::encrypt('settings/assets/transforms'),
+ ]);
+
+ post(action([ImageTransformsController::class, 'store']), $payload)
+ ->assertRedirect(Url::cpUrl('settings/assets/transforms'))
+ ->assertSessionHas('success', t('Transform saved.'));
+});
+
it('updates an existing transform', function () {
$transform = createTestTransform([
'name' => 'Original Name',
@@ -138,7 +178,7 @@ function validTransformData(array $overrides = []): array
'width' => 100,
]);
- postJson(action([ImageTransformsController::class, 'save']), validTransformData([
+ postJson(action([ImageTransformsController::class, 'store']), validTransformData([
'transformId' => $transform->id,
'name' => 'Updated Name',
'handle' => $transform->handle,
@@ -157,7 +197,7 @@ function validTransformData(array $overrides = []): array
});
it('rejects save when both width and height are missing', function () {
- postJson(action([ImageTransformsController::class, 'save']), validTransformData([
+ postJson(action([ImageTransformsController::class, 'store']), validTransformData([
'width' => '',
'height' => '',
]))
@@ -173,7 +213,7 @@ function validTransformData(array $overrides = []): array
'fill' => 'abc',
]);
- postJson(action([ImageTransformsController::class, 'save']), $payload)->assertOk();
+ postJson(action([ImageTransformsController::class, 'store']), $payload)->assertOk();
$service = app(ImageTransforms::class);
$service->reset();
diff --git a/tests/Feature/Integration/PagesTest.php b/tests/Feature/Integration/PagesTest.php
index e706c75e62f..1b2c60b5bbf 100644
--- a/tests/Feature/Integration/PagesTest.php
+++ b/tests/Feature/Integration/PagesTest.php
@@ -99,7 +99,7 @@
[
'url' => '/settings/assets/transforms',
'title' => 'Image Transforms',
- 'component' => 'SettingsImageTransformsIndexPage',
+ 'component' => 'settings/assets/transforms/ImageTransformsIndexPage',
],
]);
diff --git a/yii2-adapter/legacy/web/assets/edittransform/EditTransformAsset.php b/yii2-adapter/legacy/web/assets/edittransform/EditTransformAsset.php
index af7ff2bf291..1ced6e3e607 100644
--- a/yii2-adapter/legacy/web/assets/edittransform/EditTransformAsset.php
+++ b/yii2-adapter/legacy/web/assets/edittransform/EditTransformAsset.php
@@ -8,7 +8,7 @@
namespace craft\web\assets\edittransform;
use craft\web\AssetBundle;
-use CraftCms\Cms\View\LegacyAssets\InternalAssetRegistry;
+use craft\web\assets\cp\CpAsset;
/**
* Asset bundle for the Edit Transform page
@@ -16,8 +16,13 @@
*/
class EditTransformAsset extends AssetBundle
{
- public function registerAssetFiles($view)
- {
- app(InternalAssetRegistry::class)->register(\CraftCms\Cms\View\LegacyAssets\EditTransformAsset::class);
- }
+ public $sourcePath = '@craftcms/resources/legacy/edittransform/dist';
+
+ public $depends = [
+ CpAsset::class,
+ ];
+
+ public $css = [
+ 'css/transforms.css',
+ ];
}