PM-Editor UX: Kategorie in die Schreibfläche, Bild-Upload als Modal-Flow

Kategorie-Pflichtfeld direkt unter die Firma statt in die Sidebar.
Titelbild-Upload öffnet ein Modal: erst Bild wählen, dann erscheinen
die gestrafften Schritte (Herkunft & Bildnachweis mit Vorschlag aus
dem Urheber, Personen & Rechte, Bestätigung), Optionales eingeklappt.
Bildrechte-Checkbox im Einreichen-Modal nur noch mit eigenem Titelbild.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-07-09 13:55:21 +00:00
parent c387048d7e
commit 2882bc44b2
10 changed files with 705 additions and 391 deletions

View file

@ -1,5 +1,6 @@
<?php
use App\Enums\AiImageDeclaration;
use App\Enums\ImageLicenseType;
use App\Models\Category;
use App\Models\Company;
@ -56,11 +57,29 @@ test('license type starts with an explicit placeholder before own photo option',
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
->call('openUploadForm')
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
->assertSet('newLicenseType', '')
->assertSee('Bitte wählen')
->assertSee('Eigene Aufnahme');
});
test('detail fields only appear after an image was selected', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
$this->actingAs($owner);
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
->call('openUploadForm')
->assertSee('Bild hierher ziehen oder klicken')
->assertDontSee('Herkunft & Bildnachweis')
->assertDontSee('Personen & Rechte Dritter')
->assertDontSee('Rechte bestätigt')
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
->assertSee('Herkunft & Bildnachweis')
->assertSee('Personen & Rechte Dritter')
->assertSee('Rechte bestätigt');
});
test('title image upload form is collapsed until requested', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
@ -180,6 +199,7 @@ test('valid upload stores license metadata and confirms rights', function () {
->set('newCopyright', 'Foto: Jane Doe / Beispiel GmbH')
->set('newAuthor', 'Jane Doe')
->set('newLicenseType', ImageLicenseType::Own->value)
->set('newAiDeclaration', AiImageDeclaration::Original->value)
->set('newSourceUrl', 'https://example.com/source')
->set('newPeopleRightsStatus', 'consent')
->set('newPropertyRightsStatus', 'cleared')
@ -228,6 +248,7 @@ test('valid cc upload stores license detail and license url', function () {
->set('newAuthor', 'Jane Doe')
->set('newCopyright', 'Foto: Jane Doe (CC BY 4.0)')
->set('newLicenseType', ImageLicenseType::CreativeCommons->value)
->set('newAiDeclaration', AiImageDeclaration::Original->value)
->set('newLicenseDetail', 'cc_by')
->set('newLicenseUrl', 'https://creativecommons.org/licenses/by/4.0/')
->set('newPeopleRightsStatus', 'none')
@ -244,7 +265,7 @@ test('valid cc upload stores license detail and license url', function () {
expect($image->license_url)->toBe('https://creativecommons.org/licenses/by/4.0/');
});
test('the public image credit is required', function () {
test('the public image credit is required even when the suggestion was cleared', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
$this->actingAs($owner);
@ -253,6 +274,8 @@ test('the public image credit is required', function () {
->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800))
->set('newAuthor', 'Jane Doe')
->set('newLicenseType', ImageLicenseType::Own->value)
// Der Vorschlag aus dem Urheber wird bewusst entfernt.
->set('newCopyright', '')
->set('newPeopleRightsStatus', 'none')
->set('newPropertyRightsStatus', 'none')
->set('newRightsConfirmed', true)
@ -311,9 +334,42 @@ test('a valid ai generated upload stores tool and ai flag', function () {
expect($image->license_type)->toBe(ImageLicenseType::AiGenerated);
expect($image->license_detail)->toBe('Midjourney v7');
expect($image->is_ai_generated)->toBeTrue();
// Lizenztyp „KI-generiert" setzt die KI-Kennzeichnung automatisch.
expect($image->ai_declaration)->toBe(AiImageDeclaration::AiGenerated);
expect($image->copyright)->toBe('Bild: KI-generiert (Midjourney v7)');
});
test('the copyright suggestion follows the author but respects manual input', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
$this->actingAs($owner);
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
->set('newLicenseType', ImageLicenseType::Own->value)
->set('newAuthor', 'Jane Doe')
->assertSet('newCopyright', 'Foto: Jane Doe')
->set('newAuthor', 'John Smith')
->assertSet('newCopyright', 'Foto: John Smith')
->set('newCopyright', 'Eigener Nachweis')
->set('newAuthor', 'Jane Doe')
->assertSet('newCopyright', 'Eigener Nachweis');
});
test('switching to ai replaces an automatically suggested author copyright', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
$this->actingAs($owner);
LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
->set('newLicenseType', ImageLicenseType::Own->value)
->set('newAuthor', 'Jane Doe')
->assertSet('newCopyright', 'Foto: Jane Doe')
->set('newLicenseType', ImageLicenseType::AiGenerated->value)
->assertSet('newCopyright', 'Bild: KI-generiert')
->set('newLicenseType', ImageLicenseType::Own->value)
->assertSet('newCopyright', 'Foto: Jane Doe');
});
test('the ai copyright suggestion follows the tool but respects manual input', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();

View file

@ -1,5 +1,7 @@
<?php
use App\Enums\AiTextDeclaration;
use App\Enums\ImageLicenseType;
use App\Enums\PressReleaseStatus;
use App\Models\Plan;
use Database\Seeders\RolesAndPermissionsSeeder;
@ -58,6 +60,9 @@ test('submitting from the show modal moves the draft into review without consumi
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
// KI-Kennzeichnung (Art. 50 KI-VO) ist Pflicht im Submit-Modal.
->set('aiTextDeclaration', AiTextDeclaration::None->value)
->set('aiDeclarationConfirmed', true)
->call('submitForReview')
->assertHasNoErrors();
@ -66,6 +71,39 @@ test('submitting from the show modal moves the draft into review without consumi
expect($customer->fresh()->press_release_quota_used_this_month)->toBe(0);
});
test('the image rights confirmation is hidden while only the placeholder is active', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a();
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Ihre Bestätigungen')
->assertDontSee('Alle Bildrechte sind geklärt.')
->assertSeeHtml("' / 4'");
});
test('the image rights confirmation appears once an own title image exists', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a();
$pr->images()->create([
'disk' => 'public',
'path' => 'press-releases/'.$pr->id.'/images/title.jpg',
'variants' => ['cover' => 'press-releases/'.$pr->id.'/images/title-cover.jpg'],
'author' => 'Jane Doe',
'license_type' => ImageLicenseType::Own->value,
'rights_confirmed_at' => now(),
'is_preview' => true,
'sort_order' => 1,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Alle Bildrechte sind geklärt.')
->assertSeeHtml("' / 5'");
});
test('the modal shows a booking notice instead of the submit flow when the gate is enforced', function () {
/** @var TestCase $this */
config()->set('billing.enforce_booking', true);