Akeneo ikona technologii Akeneo PIM UI – Kompletny przewodnik dla Frontend Developerów

Akeneo PIM UI – Kompletny przewodnik dla Frontend Developerów

Akeneo PIM stanowi wyzwanie dla Frontend Developerów, którzy stawiają pierwsze kroki w tej technologii. O ile dokumentacja techniczna dotycząca backendu jest dostępna i programiści mogą do niej sięgać, by uzyskać odpowiedzi na nurtujące pytania, to część frontendowa może już sprawiać problemy. Trudno jest bowiem znaleźć oficjalne materiały, które zawierałyby komplet informacji z danego obszaru. Dodatkowo architektura frontendu Akeneo nieustannie ewoluuje, a stack technologiczny ulega zmianom – tym trudniej rozeznać się, co jest dobrą praktyką przy tworzeniu UI.

W cyklu artykułów na temat UI w Akeneo przedstawimy mechanizm działania frontendu, a także wyjaśnimy, jak modyfikować oprogramowanie, by odpowiadało naszym wymaganiom.


Przeczytaj również inne artykuły z cyklu:


Potrzebujesz PIM? Wybierz Akeneo!

Nasz przewodnik zaczniemy od kilku słów na temat samego produktu i jego przeznaczenia. Akeneo to narzędzie z rodziny Product Information Management (PIM) i ma za zadanie ułatwić zarządzanie informacją produktową. Pełni funkcję swoistego magazynu oraz managera danych. Jedną z największych zalet Akeneo PIM jest jego modułowa architektura, która pozwala na rozbudowę i dostosowanie systemu do własnych potrzeb – zarówno posiadacze Community Edition, Growth Edition, jak i Enterprise Edition mają do dyspozycji ogromną liczbę modułów wspierających różne aspekty biznesu.

Jak jest generowany frontend w Akeneo?

Akeneo jest rozwiązaniem typu open source napisanym w języku PHP (w oparciu o framework Symfony). Warstwa fronendowa aplikacji korzysta z takich technologii jak jQuery, RequireJS, TypeScript, Backbone.js oraz React.

Jak wspomnieliśmy na samym początku, frontend Akeneo to połączenie różnych technologii, starych i nowych. Tak więc jedną z części możemy określić mianem starego stacku technologicznego, do którego zaliczamy RequireJS, Backbone.js oraz jQuery. Drugą część stacku technologicznego nazwijmy jako docelowy i należą do niego webpack, React oraz TypeScript. Co ważne obecnie w najnowszej wersji Akeneo PIM 5.0 jesteśmy na etapie pośrednim. Mamy więc część frontendową wyłącznie na bazie starej technologii oraz dostrzec można elementy zbudowane już z użyciem nowej technologii. Trafiają się też takie części frontendu, które łączą stare i nowe rozwiązania.

Przyjrzyjmy się bliżej tym technologiom!

RequireJS

RequireJS nadal jest używany w sporej części aplikacji, służąc do importowania różnych zależności, jednak wraz z kolejnymi wersjami Akeneo zaczął być wypierany przez webpack (a dokładniej od wersji Akeneo PIM 2.0). Na obecną chwilę requirejs.yml oraz webpack są wykorzystywane jednocześnie.

Fragment przykładowego pliku konfiguracyjnego .yml dla RequireJS (cały plik znajdziesz tutaj):

config:
  config:
    pim/router:
      indexRoute: pim_dashboard_index
    # Forwarded events from mediator
    pim/form/common/edit-form:
      forwarded-events:
        'pim_enrich:form:field:extension:add': 'pim_enrich:form:field:extension:add'
        'pim_enrich:form:filter:extension:add': 'pim_enrich:form:filter:extension:add'
        'pim_enrich:form:entity:pre_save':     'pim_enrich:form:entity:pre_save'
        'pim_enrich:form:entity:update_state': 'pim_enrich:form:entity:update_state'
        'pim_enrich:form:entity:post_fetch':   'pim_enrich:form:entity:post_fetch'
    pim/cache-invalidator:
      events:
      - 'pim_enrich:form:entity:post_fetch'
    pim/job/product/edit/content:
      forwarded-events:
        'pim_enrich:form:filter:extension:add': 'pim_enrich:form:filter:extension:add'
    pim/grid/view-selector/selector:
      forwarded-events:
        'grid:product-grid:state_changed': 'grid:product-grid:state_changed'
    pim/controller/group:
      fetcher: group
    pim/remover/association-type:
      url: pim_enrich_associationtype_rest_remove
    pim/remover/attribute-group:
      url: pim_enrich_attributegroup_rest_remove
    pim/remover/group-type:
      url: pim_enrich_grouptype_rest_remove
    pim/remover/channel:
      url: pim_enrich_channel_rest_remove

Backbone.js

Za warstwę wizualną w Akeneo odpowiada również Backbone.js, choć powoli jest wypierany przez React. Obecnie framework ten pomaga w tworzeniu podstawowych widoków oraz części formularzy.

RequireJS z użyciem gotowych klas napisanych w Backone.js:

'use strict';

define([
  'oro/translator',
  'backbone',
  'oro/mediator',
  'pim/form',
  'pim/fetcher-registry',
  'pim/template/common/default-template',
], function (__, Backbone, mediator, BaseForm, FetcherRegistry, template) {
  return BaseForm.extend({
    template: _.template(template),

    /**
     * {@inheritdoc}
     */
    initialize: function () {
      this.model = new Backbone.Model({});

      BaseForm.prototype.initialize.apply(this, arguments);
    },

    /**
     * {@inheritdoc}
     */
    configure: function () {
      Backbone.Router.prototype.once('route', this.unbindEvents);

      if (_.has(__moduleConfig, 'forwarded-events')) {
        this.forwardMediatorEvents(__moduleConfig['forwarded-events']);
      }

      return BaseForm.prototype.configure.apply(this, arguments);
    },

    /**
     * {@inheritdoc}
     */
    render: function () {
      if (!this.configured) {
        return this;
      }

      this.getRoot().trigger('oro_config:form:render:before');

      this.$el.html(this.template());

      this.renderExtensions();

      this.getRoot().trigger('oro_config:form:render:after');

      return this;
    },

    /**
     * Clear the mediator events
     */
    unbindEvents: function () {
      mediator.clear('oro_config:form');
    },
  });
});

Zastanawiasz się może, jak rozpoznać, że to RequireJS z wykorzystaniem Backbone? Zwróć uwagę, że Backbone jest importowany jako druga dependencja przez RequireJS. Po drugie metoda "initialize" wywołuje nową instancję modelu Backbone, a do tego można zauważyć, że w innych funkcjach jest odniesienie do innych metod Backbone takich jak "Backbone.Router".

Ponadto widzimy tutaj, że jako kolejną dependencję dodano "BaseForm". Jest to jedna z podstawowych klas Akeneo, którą można potraktować jako model przy tworzeniu wielu innych klas. Plik ten znajduje się tutaj i jest rozszerzeniem "Backbone.View".

Poniżej przedstawiamy modyfikację pliku "create-button.js" z zastosowaniem klasy "BaseForm", o której była mowa wcześniej. W naszej modyfikacji posługujemy się dodatkowym plikiem "maco-product-state", który przechowuje pewne dane dotyczące produktu np. cenę (w kolejnych rozdziałach opiszemy, jak tworzyć i dodawać pliki oraz moduły). Z danego pliku pobieramy informację o Id produktu i rodzinie (patrz: linijki kodu 56-57), a za pomocą "setData" ustawiamy te wartości.

'use strict';

define(
    [
        'jquery',
        'underscore',
        'oro/translator',
        'pim/form',
        'macopedia/templates/product/transform-to-variant',
        'routing',
        'pim/dialogform',
        'pim/form-builder',
        'oro/mediator',
        'maco-product-state'
    ],
    function (
        $,
        _,
        __,
        BaseForm,
        template,
        Routing,
        DialogForm,
        FormBuilder,
        mediator,
        ProductState
    ) {
        return BaseForm.extend({
            template: _.template(template),
            className: 'AknDropdown-menuLink transform-to-variant',
            dialog: null,
            productId: null,
            familyId: null,
            variantAxis: null,

            /**
             * {@inheritdoc}
             */
            initialize: function (config) {
                this.config = config.config;
                BaseForm.prototype.initialize.apply(this, arguments);
            },

            /**
             * {@inheritdoc}
             */
            render: function () {
                this.$el.html(this.template({
                    title: __(this.config.title),
                }));

                this.$el.on('click', function () {
                    FormBuilder.build(this.config.modalForm)
                        .then(function (modal) {
                            let state = {
                                productId: ProductState.getProductId(),
                                familyId: ProductState.getFamilyId(),
                                variantAxis: this.variantAxis
                            }
                            modal.setData(state);
                            modal.open();
                        }.bind(this)).catch(reason => {
                        console.error(reason);
                    })
                }.bind(this));

                return this;
            }
        });
    });

jQuery

Tworząc frontend w Akeneo, możemy zastosować jQuery. Pomimo rosnącej popularności innych frameworków, nadal pozostaje jedną z najbardziej rozpoznawalnych i najczęściej wykorzystywanych bibliotek JavaScript. W Akeneo jQuery pojawia się w starszych elementach, które nie zostały jeszcze przepisane do Reacta lub czasem nawet razem z Reactem. Używamy go np. przy obsłudze pól "select" wraz z biblioteką "select2".

Połączenie jQuery, TypeScript i React w komponencie odpowiadającym za tworzenie pola "select":

componentDidMount() {
        if (null === this.DOMel.current) {
            return;
        }

        this.el = $(this.DOMel.current);

        if (undefined !== this.el.select2) {

            this.initSelectField();

            if(!this.el.val()) {
                this.el.select2('val', -1);
            }

            this.el.on('change', (event: any) => {
                const newValue = this.props.multiple
                    ? event.val.map((recordCode: string) => RecordCode.create(recordCode))
                    : '' === event.val
                        ? null
                        : RecordCode.create(event.val);
                this.props.onChange(newValue);
            });

            // Prevent the onSelect event to apply it even when the options are null
            const select2 = this.el.data('select2');
            select2.onSelect = (function(fn) {
                return function(_data: any, options: any) {
                    if (null === options || 'A' !== options.target.nodeName) {
                        fn.apply(this, arguments);
                    }
                };
            })(select2.onSelect);
        } else {
            this.el.prop('type', 'text');
        }
    }

TypeScript

TypeScript został stworzony przez firmę Microsoft w modelu open source jako nadzbiór języka JavaScript. W ten sposób otrzymaliśmy statycznie typowany język programowania, który umożliwia też kodowanie zorientowane obiektowo oparte na klasach. TypeScript jest wykorzystywany wraz z Reactem w nowszych warstwach aplikacji.

Deklaracja typów w TypeScript:

import {ButtonHTMLAttributes, ReactNode} from "react";

export type FormValue = {
    [key: string]: string;
};

export type FormProps = {
    children?: ReactNode;
    value: FormValue;
    onChange: (value: FormValue) => void;
};

export type OptionProps = {
    value: string;
    isSelected?: boolean;
    isDisabled?: boolean;
    children?: ReactNode;
    onSelect?: () => void;
} & ButtonHTMLAttributes<htmlbuttonelement>;

export type QuickExportConfiguratorProps = {
    showWithLabelsSelect: boolean;
    showWithMediaSelect: boolean;
    onActionLaunch: (formValue: FormValue) =&gt; void;
    getProductCount: () =&gt; number;
};

export type SelectProps = {
    children?: ReactNode;
    name: string;
    value?: string | null;
    isVisible?: boolean;
    onChange?: (value: string | null) =&gt; void;
};</htmlbuttonelement>

React

Coraz więcej elementów aplikacji jest przypisywanych na React.js. W plikach źródłowych Akeneo znajdziemy już dziesiątki, jeśli nawet nie setki komponentów. Przykładem może być komponent odpowiadający za upload zdjęć.

Kod odpowiadający za renderowanie uploadera do zdjęć:

return (
    <container>
      {1  items.length ? (
        &gt;
          <styledmultiplebutton onclick="{()"> setOpen(true)}&gt;
            <span>{children}</span>
            <downbutton>
              <arrowdownicon size="{18}"></arrowdownicon>
            </downbutton>
          </styledmultiplebutton>
          {isOpen &amp;&amp; (
            &gt;
              <backdrop onclick="{()"> setOpen(false)} /&gt;
              <panel>
                {items.map(item =&gt; (
                  <item key="{item.label}" title="{item.title" isdisabled="{item.isDisabled}" onclick="{()"> onItemClick(item)}
                  &gt;
                    {item.label}
                  </item>
                ))}
              </panel>
            <!--</>-->
          )}
        <!--</>-->
      ) : (
        <button onclick="{items[0].action}">
          {items[0].label}
        </button>
      )}
    </backdrop></container>
  );

Storybook

Wraz z rozwojem warstwy frontendowej Akeneo i stopniowego przechodzenia na React wdrożono również Storybook, czyli dokumentację komponentów UI. Z jej pomocą w łatwy sposób sprawdzisz, jakie komponenty są dostępne w projekcie, jak wyglądają oraz jakie mają możliwości.

Tutaj znajdziesz link do oficjalnego Storybooka Akeneo:

https://dsm.akeneo.com/?path=/story/introduction--pag

W kolejnych artykułach przybliżymy inne wyzwania stojące przed deweloperami. Przygotuj się na więcej!