Bild wird in CMS Element in der Administration nicht richtig angezeigt

Hallo,

ich kämpfe jetzt schon länger mit einem Problem welches meine eigenen CMS Elemente mit Bild(ern) betrifft: Und zwar wird in der Administration manchmal die falsche Bild-URL für das gewählte Bild ausgegeben: Nach dem neu-auswählen eines Bilds funktioniert es, aber oft nach dem Speichern der Erlebniswelt oder nach einem Refresh wird das Fallback-Bild ( preview_mountain_large.jpg ) angezeigt, so als ob das gewählte Bild keine URL hätte. In der Config ist das Bild aber noch richtig hinterleget, und im Shop Frontend funktioniert es immer richtig.

Daher glaube ich es muss hieran liegen:

im index.js:

import { Component, Mixin } from "src/core/shopware";
import template from "./big-teaser.html.twig";

Component.register("gg-big-teaser", {
    template,

    mixins: [Mixin.getByName("cms-element")],

    computed: {
        imageDesktopUrl() {
            return this.getImageUrl(
                this.element.data.imageDesktop,
                this.element.config.imageDesktop
            );
        }
    },

    watch: {
        cmsPageState: {
            deep: true,
            handler() {
                this.$forceUpdate();
            },
        },
    },

    created() {
        this.createdComponent();
    },

    methods: {
        createdComponent() {
            this.initElementConfig("big-teaser");
            this.initElementData("big-teaser");
        },

        getImageUrl(dataImage, configImage) {
            const context = Shopware.Context.api;
            const elemData = dataImage;
            if (configImage.source === "mapped") {
                const demoMedia = this.getDemoValue(configImage.value);
                if (demoMedia && demoMedia.url) {
                    return demoMedia.url;
                }
            }
            if (elemData && elemData.id) {
                return dataImage.url;
            }
            if (elemData && elemData.url) {
                return `${context.assetsPath}${elemData.url}`;
            }
            return `${context.assetsPath}/administration/static/img/cms/preview_mountain_large.jpg`;
        },
    },
});

im Template: 

 

Hab ich hier irgendwo einen Fehler drinnen den ich nicht finde, oder ist es ein Bug von Shopware? Danke schonmal!

Im Template referenzierst du myImageUrl, was im JS nigrends definiert ist. Eventuell getImageUrl verwenden?

@cstore schrieb:

Im Template referenzierst du myImageUrl, was im JS nigrends definiert ist. Eventuell getImageUrl verwenden?

 Da hab ich wohl falsch aus meinem Code kopiert, tut mir Leid. Natürlich verwende ich im Template „imageDesktopUrl“ - sonst würde es ja gar nicht gehen.

Dann fällt mir ad hoc nur Detektivarbeit ein.  Undecided Vielleicht kommst du der Sache mit 

console.log(dataImage);
console.log(configImage);

oben in deiner getImageUrl() auf die Spur. Füg auch vor dem letzten return in derselben Funktion mal ein console.log(“test”) ein, um zu gucken, ob es tatsächlich dieses letzte return ist, durch welches das Mountain-Image zur Anzeige kommt. (Der Output von console.log ist zu sehen in der Entwicklerkonsole des Browsers, auch wenn du es vermutlich bereits weißt.) Viel Erfolg!

Danke für den Hinweis! Es scheint so zu sein:

Wenn ich ein neues Bild auswähle wird das zweite Return-Statement ausgeführt.

Nach Speichern/Neuladen geht es eben nicht: Das Mountain-Image kommt tatsächlich vom letzten Return-Statement, und zwar weil _ dataImage _ dann null ist. (configImage scheint richtig zu sein.)

Evtl hat es mit meiner config/index.js zu tun?

import { Component, Mixin } from "src/core/shopware";
import template from "./big-teaser-config.html.twig";

Component.register("gg-big-teaser-config", {
    template,

    mixins: [Mixin.getByName("cms-element")],

    inject: ["repositoryFactory"],

    data() {
        return {
            mediaModalIsOpen: false,
            initialFolderId: null,
        };
    },

    computed: {
        imageDesktopPreview() {
            return this.getImagePreview(
                this.element.data.imageDesktop,
                this.element.config.imageDesktop
            );
        },
    },

    created() {
        this.createdComponent();
    },

    methods: {
        createdComponent() {
            this.initElementConfig("big-teaser");
            this.initElementData("big-teaser");
        },

        // Image Methods start here:

        setValueOfImage(value) {
            switch (this.activeImageVariable) {
                case "imageDesktop":
                    this.element.config.imageDesktop.value = value;
                    break;
            }
        },

        getImagePreview(dataImage, configImage) {
            if (this.element.data && dataImage && dataImage.id) {
                return dataImage;
            }
            return configImage.value;
        },

        onImageRemove(variableName) {
            this.activeImageVariable = variableName;
            this.setValueOfImage(null);
            this.updateElementData();
            this.$emit("element-update", this.element);
        },

        onCloseModal() {
            this.mediaModalIsOpen = false;
            this.activeImageVariable = null;
        },

        onSelectionChanges(mediaEntity) {
            const media = mediaEntity[0];
            this.setValueOfImage(media.id);
            this.updateElementData(media);
            this.$emit("element-update", this.element);
        },

        updateElementData(media = null) {
            this.$set(
                this.element.data,
                this.activeImageVariable + "Id",
                media === null ? null : media.id
            );
            this.$set(this.element.data, this.activeImageVariable, media);
        },

        onOpenMediaModal(variableName) {
            this.mediaModalIsOpen = true;
            this.activeImageVariable = variableName;
        },
    },
});

 

Du kannst mal mittels {{ dump(element) }} in der Storefront gucken, ob dort ein data-Attribut verfügbar ist; ich vermute nein. Entsprechend funktioniert es in der Storefront – vermutlich – deswegen, weil dein Bild über Informationen im Config-Attribut geladen wird und nicht über Informationen im data-Attribut.

Das data-Attribut bei eigenen CMS-Elementen wird m.W. nämlich nur dann aus gespeicherten Daten befüllt, wenn man einen eigenen Data-Resolver für das CMS-Element einrichtet, https://github.com/stefanpoensgen/swag-docs-custom-cms-element.

Wenn du glaubst, du brauchst einen DataResolver, schreib hier nochmal; ich hatte mal für mich selbst grob aufgeschrieben, wie das geht, und kann das hier teilen.

@etoile schrieb:

Du kannst mal mittels {{ dump(element) }} in der Storefront gucken, ob dort ein data-Attribut verfügbar ist; ich vermute nein. Entsprechend funktioniert es in der Storefront – vermutlich – deswegen, weil dein Bild über Informationen im Config-Attribut geladen wird und nicht über Informationen im data-Attribut.

Das data-Attribut bei eigenen CMS-Elementen wird m.W. nämlich nur dann aus gespeicherten Daten befüllt, wenn man einen eigenen Data-Resolver für das CMS-Element einrichtet, https://github.com/stefanpoensgen/swag-docs-custom-cms-element.

Wenn du glaubst, du brauchst einen DataResolver, schreib hier nochmal; ich hatte mal für mich selbst grob aufgeschrieben, wie das geht, und kann das hier teilen.

Tatsächlich, element.data ist im Frontend null, die Bilder lese ich mir über die Config aus:

{% set imageDesktopId = element.config.imageDesktop.value %}
{% set imageDesktop = searchMedia([imageDesktopId], context.context).get(imageDesktopId) %}

Ich hab jetzt versucht einen Data Resolver zu erstellen so wie es hier beschrieben ist: Custom CmsElement: Bilder werden nicht richtig ausgelesen - Programmierung - Shopware Community Forum , hat aber leider noch nicht funktioniert. Ich würde dir sehr dankbar sein wenn du mir dabei helfen könntest. :) 

Okay, wie vermutet. :) Hier meine private Zusammenfassung, die sehr nahe ans Ziel führt:

Data-Objekte in Storefront verfügbar machen (ohne Gewähr)
=========================================================

Damit in den Storefront-Templates die Variable element.data Inhalt hat:
eigenen DataResolver für das jeweilige Cms-Element implementieren.

Maßgebliches Beispiel-Repo: https://github.com/stefanpoensgen/swag-docs-custom-cms-element
(Alle Lorbeeren an dessen Ersteller!)

Hier: reduzierter Bauplan für ein minimales Daten-Objekt. Für brauchbare Funktion sind weitere Einzelheiten im Beispiel-Repo nachzusehen.

Zum Testen: In die storefront-twig-Datei des CMS-Elements oder des umgebenden Blocks ein dump(element) [oder so ähnlich] einbauen.
---

Wir nehmen ein Plugin mit dem Namespace Swag\FancyStore und ein CMS-Element namens fancy-banner an.

Dateien:
- Wurzel-PHP-Datei des Plugins erweitern (um die funktion "build")
- Verzeichnisse anlegen: /src/Core/Content mit den drei Unterverzeichnissen Media/Cms/Type (für Resolver), DependencyInjection (für media.xml) und SalesChannel/Struct (für Daten-Struct)
- Dateien anlegen: in .../Struct die Datei SwagElementStruct.php, in .../Type die Datei SwagElementTypeDataResolver.php, in .../DependencyInjection die Datei media.xml


Minimale SwagElementTypeDataResolver-Klasse (Use-Statements sind der Kürze halber ausgelassen; 
sind per Abgleich mit Beispiel-Repo einzufügen):
namespace ...
use ...
...
class FancyBannerTypeDataResolver extends AbstractCmsElementResolver
{
	public function getType(): string { return 'fancy-banner'; }
	public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection { return null; }
	public function enrich(CmsSlotEntity $slot, Resolver Context $resolver Context, ElementDataCollection $result): void
	{ $config = $slot->getFieldConfig(); $fancyBannerData = new FancyBannerStruct(); $slot->setData($fancyBannerData); }
}


Minimale FancyBannerStruct-Klasse – für ein data-Objekt mit einer einzigen String-Variable 'url':
namespace ...

use Shopware\Core\Framework\Struct\Struct;
[+ eigene Zusätze]

class FancyBannerStruct extends Struct
{
		/**
		 * @var string|null
		 */
		protected $url;

		public function getUrl(): ?string
		{
			return $this->url;
		}
	
		public function setUrl(?string $url): void
		{
			$this->url = $url;
		}
	
		public function getApiAlias(): string
		{
			return 'cms_fancyBanner'; // Anmerkung: hier weiß ich nicht, ob statt CamelCase etwa Unterstriche zur Worttrennung zu verwenden sind.
		}
}


Minimale media.xml-Datei:

Zusätzliche Daten-Felder sind dann vor allem durch Erweiterung des Structs sowie der Funktion enrich() implementierbar.

 

Wobei es in deinem Fall einfacher sein könnte, so wie Stefan Poensgen im von dir verlinkten Thread schrieb, den Shopware-eigenen Resolver zu extenden statt einen komplett eigenen auf Basis des AbstractCmsElementResolver zu schreiben.

Danke, ich hab jetzt im Frontend meine Daten in element.data, jedoch scheint das ganze an den verfügbaren Daten in der Administration (also in this.element.data in der Komponente) nichts verändert zu haben: Die richtige Ausgabe von den Bildern funktioniert weiterhin nur wenn nur ein Bild ausgewählt ist (Mein Element hat zwei Bilder).

Fürs Verständnis: Sollten das element.data welches im Twig verfügbar ist und das this.element.data im Javascript den gleichen Inhalt haben?

Das weiß ich leider gerade nicht genau, ob der Resolver auch im Admin für die Befüllung von element.data sorgt. Ich glaube, dafür muss man noch etwas mehr konfigurieren (etwa in der config/index.js-Datei und dann nochmal in der component/index.js).

Das hier ist Code aus einem alten Projekt von mir, den ich, soweit ich mich erinnere, aus dem Shopware-eigenen Image-CMS-Element abgekupfert habe. Am relevantesten dürften die Teile mit repositoryFactory und this.element.data & updateElementData sowie eventuel $emit sein.

// config/index.js
import template from './sw-cms-el-config-smart-banner.html.twig';
import './sw-cms-el-config-smart-banner.scss';

const { Component, Mixin } = Shopware;

Component.register('sw-cms-el-config-smart-banner', {
    template,

    mixins: [
        Mixin.getByName('cms-element')
    ],

    inject: ['repositoryFactory'],

    data() {
        return {
            mediaModalIsOpen: false,
            initialFolderId: null
        };
    },

    computed: {
        mediaRepository() {
            return this.repositoryFactory.create('media');
        },

        uploadTag() {
            return `cms-element-media-config-${this.element.id}`;
        },

        previewSource() {
            if (this.element.data && this.element.data.media && this.element.data.media.id) {
                return this.element.data.media;
            }

            return this.element.config.media.value;
        }
    },

    created() {
        this.createdComponent();
    },

    methods: {
        createdComponent() {
            this.initElementConfig('smart-banner');
        },

        async onImageUpload({ targetId }) {
            const mediaEntity = await this.mediaRepository.get(targetId, Shopware.Context.api);

            this.element.config.media.value = mediaEntity.id;

            this.updateElementData(mediaEntity);

            this.$emit('element-update', this.element);
        },

        onImageRemove() {
            this.element.config.media.value = null;

            this.updateElementData();

            this.$emit('element-update', this.element);
        },

        onCloseModal() {
            this.mediaModalIsOpen = false;
        },

        onSelectionChanges(mediaEntity) {
            const media = mediaEntity[0];
            this.element.config.media.value = media.id;

            this.updateElementData(media);

            this.$emit('element-update', this.element);
        },

        updateElementData(media = null) {
            this.$set(this.element.data, 'mediaId', media === null ? null : media.id);
            this.$set(this.element.data, 'media', media);
        },

        onOpenMediaModal() {
            this.mediaModalIsOpen = true;
        },



        // AM ENDE alle hierunter wohl RAUS:
        onChangeMinHeight(value) {
            this.element.config.minHeight.value = value === null ? '' : value;

            this.$emit('element-update', this.element);
        },


        onChangeDisplayMode(value) {
            if (value === 'cover') {
                this.element.config.verticalAlign.value = null;
            } else {
                this.element.config.minHeight.value = '';
            }

            this.$emit('element-update', this.element);
        }
    }
});

Ich vermute, dass in der Tat this.element.data aus dem JS deckungsgleich mit  element.data im Twig ist, aber bin ad hoc nicht hundertprozentig sicher damit.

Ja, dieses Zeug hab ich genauso am Image Element abgeschaut, und so angepasst dass es mit mehreren Bildern funktioniert - anscheinend hab ich da irgendetwas falsch gemacht. Allerdings funktioniert eigentlich alles, bis die Erlebniswelt eben neu geladen wird (z.B. durchs Speichern) - dort geht dann ein Bild immer verloren.

(Ich hab den Code von meiner config/index.js oben in einem Kommentar drinnen.)

(Ah, stimmt, da ist ja schon dein Code.) Mir scheint es bei erster Betrachtung, dass du die Zwei-Bilder-Sache nicht konsequent durchgezogen hast. Zum Beispiel fehlt in diesem Teil aus deinem Code doch der zweite case, oder?

        setValueOfImage(value) {
            switch (this.activeImageVariable) {
                case "imageDesktop":
                    this.element.config.imageDesktop.value = value;
                    break;
            }
        },

Ich würde angesichts dessen darauf tippen, dass es nicht zufällig ist, welches Bild immer verloren geht, sondern stets das gleiche (nämlich nicht_ _imageDesktop, sondern das andere), oder?

Das ist mal wieder mein Fehler beim Code-im-Forum posten, den Teil mit imageMobile hatte ich rausgenommen weil ich gedacht habe es wäre irrelevant. Hier die ganze Datei, unverändert:

import { Component, Mixin } from "src/core/shopware";
import template from "./big-teaser-config.html.twig";

Component.register("gg-big-teaser-config", {
    template,

    mixins: [Mixin.getByName("cms-element")],

    inject: ["repositoryFactory"],

    data() {
        return {
            mediaModalIsOpen: false,
            initialFolderId: null,
        };
    },

    computed: {
        imageDesktopPreview() {
            return this.getImagePreview(
                this.element.data.imageDesktop,
                this.element.config.imageDesktop
            );
        },

        imageMobilePreview() {
            return this.getImagePreview(
                this.element.data.imageMobile,
                this.element.config.imageMobile
            );
        },
    },

    created() {
        this.createdComponent();
    },

    methods: {
        createdComponent() {
            this.initElementConfig("big-teaser");
            this.initElementData("big-teaser");
        },

        // Image Methods start here:

        setValueOfImage(value) {
            switch (this.activeImageVariable) {
                // Add all possible images here
                case "imageDesktop":
                    this.element.config.imageDesktop.value = value;
                    break;
                case "imageMobile":
                    this.element.config.imageMobile.value = value;
                    break;
                default:
                    console.error(
                        "You forgot to add your image to the switch-case statement in the config index.js!"
                    );
            }
        },

        getImagePreview(dataImage, configImage) {
            if (this.element.data && dataImage && dataImage.id) {
                return dataImage;
            }
            return configImage.value;
        },

        onImageRemove(variableName) {
            this.activeImageVariable = variableName;
            this.setValueOfImage(null);
            this.updateElementData();
            this.$emit("element-update", this.element);
        },

        onCloseModal() {
            this.mediaModalIsOpen = false;
            this.activeImageVariable = null;
        },

        onSelectionChanges(mediaEntity) {
            const media = mediaEntity[0];
            this.setValueOfImage(media.id);
            this.updateElementData(media);
            this.$emit("element-update", this.element);
        },

        updateElementData(media = null) {
            this.$set(
                this.element.data,
                this.activeImageVariable + "Id",
                media === null ? null : media.id
            );
            this.$set(this.element.data, this.activeImageVariable, media);
            console.log("UPDATED DATA:");
            console.log(this.element.data);
        },

        onOpenMediaModal(variableName) {
            this.mediaModalIsOpen = true;
            this.activeImageVariable = variableName;
        },
    },
});

Es ist aber das imageDesktop was nicht funktioniert wenn das imageMobile definiert ist. Wenn kein imageMobile definiert ist funktioniert das imageDesktop. Komischweise funktioniert imageMobile auch wenn imageDesktop definiert ist.

Verstehe.

Da es unmittelbar nach Konfigurationsänderung funktioniert, ist die config.js vermutlich gar nicht das Problem. (Hier wird element.data anlassabhängig, nämlich bei jeder Änderung neu befüllt.)

Das Problem tritt ausschließlich beim Neuladen des Admins auf? Dann könnte es viel eher an der component/index.js liegen, wie oben auch schon ins Spiel gebracht. (Hier muss element.data auf andere Weise befüllt werden. Hier musst du vielleicht nochmal ganz genau gucken, ob der Code aus dem Shopware-Image-Element richtig übertragen und angepasst wurde.)

Ich verabschiede mich erstmal, um an eigenen Sachen weiterzuarbeiten. Viel Glück dir!

1 „Gefällt mir“

Ich hab das Problem jetzt mit einem anderen Aufbau meines Elements gelöst:

Ich hab das Element „image-viewport“ aus diesem Beispiel nachgebaut: GitHub - stefanpoensgen/swag-docs-custom-cms-element und dann so angepasst dass statt xs, sm, md etc. Namen wie „imageDesktop“ und „imageMobile“ verwendet werden. Damit ich den Resolver nicht für jedes Element einzeln schreiben muss hab ich ihn nur einmal drinnen, und für jedes Element habe ich einen Resolver gemacht der dann davon ableitet.

Resolver

getFieldConfig();
        $imagesConfig = $config->get('images');

        if (!$imagesConfig || $imagesConfig->isMapped()) {
            return null;
        }

        $images = $imagesConfig->getValue();

        $mediaIds = array_column($images, 'id');

        $criteria = new Criteria($mediaIds);

        $criteriaCollection = new CriteriaCollection();
        $criteriaCollection->add('media_' . $slot->getUniqueIdentifier(), MediaDefinition::class, $criteria);

        return $criteriaCollection;
    }

    public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, ElementDataCollection $result): void
    {
        $config = $slot->getFieldConfig();
        $image = new ImageStruct();
        $slot->setData($image);

        if ($images = $config->get('images')) {
            $images = $this->sortImages($images->getValue());

            $tmpImage = $images[0];
            foreach (self::NAMES as $name) {
                if (in_array($name, array_column($images, 'name'), true)) {
                    $key = array_search($name, array_column($images, 'name'), true);
                    $tmpImage = $images[$key];
                }
            }

            foreach ($this->sortImages($images) as $item) {
                $this->addMediaEntity($slot, $image, $result, $item);
            }
        }
    }

    private function addMediaEntity(
        CmsSlotEntity $slot,
        ImageStruct $imageStruct,
        ElementDataCollection $result,
        array $config
    ): void {
        $imageItemStruct = new ImageItemStruct();

        $searchResult = $result->get('media_' . $slot->getUniqueIdentifier());
        if (!$searchResult) {
            return;
        }

        /** @var MediaEntity|null $media */
        $media = $searchResult->get($config['mediaId']);
        if (!$media) {
            return;
        }

        $imageItemStruct->setMedia($media);
        $imageItemStruct->setName($config['name']);
        $imageStruct->addImage($imageItemStruct);
    }

    private function sortImages(array $images): array
    {
        usort($images, static function (array $a, array $b) {
            $keyA = array_search($a['name'], self::NAMES, true);
            $keyB = array_search($b['name'], self::NAMES, true);

            return $keyA <=> $keyB;
        });

        return $images;
    }
}

Resolver für Element:

*/

Services.xml enthält:
 

Example Element index.js enthält:

Shopware.Service("cmsService").registerCmsElement({
    name: "example",
    ...
    defaultConfig: {
        ....
        },
        images: { // do not change this name
            source: 'static',
            value: [],
            required: true,
            entity: {
                name: 'media'
            }
        }
    },

    // Load images:
    enrich: function enrich(elem, data) {
        if (Object.keys(data).length < 1) {
            return;
        }

        Object.keys(elem.config).forEach((configKey) => {
            const entity = elem.config[configKey].entity;

            if (!entity) {
                return;
            }

            const entityKey = entity.name;
            if (!data[`entity-${entityKey}`]) {
                return;
            }

            elem.data[configKey] = [];
            elem.config[configKey].value.forEach((image) => {
                elem.data[configKey].push({
                    name: image.name,
                    media: data[`entity-${entityKey}`].get(image.mediaId)
                });
            });
        });
    }
});

usw.