Pick images, pages, forms, sections and terms instead of typing ids and addresses

The share image and featured image of a page become image fields with the library dialog,
as do image settings of plugins. The editor offers forms, sections, categories and tags as
lists, a page search next to every link target and in text links, the redirect target has a
page search, and the form editor picks the reply field, the condition and the option images.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 09:35:05 +02:00
1 parent 31fb53c1fc
commit 711af312af
19 files changed
+446 -43

No files matched your search

+53
View File
@@ -9,6 +9,7 @@ use App\Content\MediaLibrary;
use App\Content\MenuRepository;
use App\Content\PageRepository;
use App\Content\PartialRepository;
use App\Content\PostRepository;
use App\Content\SiteSettings;
use App\Content\Theme;
use App\Domain\InvalidForm;
@@ -268,6 +269,35 @@ final class EditorSecurityTest extends WebTestCase
}
}
public function testImageSettingsOfAPostArePickedFromTheLibraryNotTypedAsAnId(): void
{
// Umut: „nicht dein ernst? ID aus Mediathek? schöne auswahl rein! auch bei Beitragsbild"
$client = $this->login('editor');
$pages = new PageRepository($this->db, new DocumentValidator(), new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')));
$id = $pages->createPage('mit-bild', null, 'post', DemoContent::pages()['home']);
$media = str_repeat('c', 32);
$this->db->insert('media', ['id' => $media, 'title' => 'Klassenzimmer', 'alt' => '', 'variants' => json_encode([['src' => '/media/'.$media.'/480', 'width' => 480, 'height' => 320, 'bytes' => 1]]), 'archived' => 0, 'version' => 1, 'created_at' => gmdate('Y-m-d H:i:s')]);
$this->db->executeStatement('UPDATE page SET featured_media_id = ? WHERE id = ?', [$media, $id]);
$crawler = $client->request('GET', '/admin/posts');
self::assertResponseIsSuccessful();
self::assertStringNotContainsString('ID aus der Mediathek', $client->getResponse()->getContent());
self::assertCount(1, $crawler->filter('script[src^="/assets/media-field.js"]'));
// Das gewählte Beitragsbild: Kennung versteckt, Vorschau und Titel sichtbar.
$featured = $crawler->filter('input[name="featuredMediaId"]')->closest('[data-media-field]');
self::assertCount(1, $featured);
self::assertSame('hidden', $featured->filter('input[name="featuredMediaId"]')->attr('type'));
self::assertSame($media, $featured->filter('input[name="featuredMediaId"]')->attr('value'));
self::assertSame('/media/'.$media.'/480', $featured->filter('img')->attr('src'));
self::assertStringContainsString('Klassenzimmer', $featured->text());
self::assertStringContainsString('Anderes Bild wählen', $featured->text());
// Ohne Vorschaubild: Wählen statt Kennung eintippen.
$og = $crawler->filter('input[name="ogMediaId"]')->closest('[data-media-field]');
self::assertCount(1, $og);
self::assertStringContainsString('Bild wählen oder hochladen', $og->text());
self::assertNull($og->filter('img')->attr('src'));
}
public function testLiveSearchReturnsFilteredEscapedResultsWithTheSamePermissionsAsTheFullScreen(): void
{
$client = $this->login('editor');
@@ -653,6 +683,29 @@ final class EditorSecurityTest extends WebTestCase
self::assertSame('Kontakt', $this->db->fetchOne('SELECT name FROM form'));
}
public function testTheEditorPicksFormsSectionsAndTermsFromListsInsteadOfTypedAddresses(): void
{
// Was ein Baustein nur verknüpft, kommt als Liste Adresse => Name - der Editor macht
// daraus Auswahlfelder statt Textfelder für Adressen.
$media = new MediaLibrary($this->db, $this->path.'-media');
$pages = new PageRepository($this->db, new DocumentValidator(), new SiteSettings($this->db, $media));
(new FormRepository($this->db))->save('anfrage', 'Anfrage', [['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true, 'help' => '', 'options' => []]], ['recipient' => 'info@example.com'], 0);
$band = DemoContent::starter('Werbeband');
$band['blocks'] = [['id' => 'band-text', 'type' => 'text', 'props' => ['text' => 'Aktion']]];
(new PartialRepository($this->db, new DocumentValidator()))->create('section', 'band', 'Werbeband', $band);
$post = $pages->createPage('ein-beitrag', null, 'post', DemoContent::pages()['home']);
(new PostRepository($this->db, $media, new SiteSettings($this->db, $media)))->saveMetadata($post, [], ['Projekte'], ['Umbau'], $pages->findForEditor($post)['version']);
$client = $this->login('editor');
$client->request('GET', '/admin/api/design');
self::assertResponseIsSuccessful();
$choices = json_decode($client->getResponse()->getContent(), true)['choices'];
self::assertSame(['anfrage' => 'Anfrage'], $choices['forms']);
self::assertSame(['band' => 'Werbeband'], $choices['sections']);
self::assertSame(['projekte' => 'Projekte'], $choices['categories']);
self::assertSame(['umbau' => 'Umbau'], $choices['tags']);
}
public function testStylePresetsNeedAnEditorRoleACsrfTokenAndAValidStyle(): void
{
$client = $this->login('editor');