Skip to content

How screens are replaced

Nebula replaces core admin screens rather than restyling them. This page explains the mechanisms in the order a request meets them.

1. The theme is forced

Nebula\Core\Plugin\ForceAdminTheme is a before plugin on Magento\Theme\Model\View\Design::setDesignTheme(). For the adminhtml area it substitutes Nebula/backend for whatever theme was requested. The theme has no parent, so nothing from Magento/backend is in the page: no RequireJS, no Knockout, no LESS-compiled stylesheet.

A second plugin, ScopeAdminLayoutToNebula, sits on the layout file collector. When any theme other than Nebula or a child of it is rendering, it drops every layout XML file from modules whose name starts with Nebula_Core. That is what lets the theme switcher return a clean stock admin: Nebula's layout instructions do not follow the merchant into Magento/backend. The prefix list is a constructor argument, so a partner package can join the gate.

2. Core UI Components are skipped, selectively

Nebula_UiRemoval has two plugins, one on the layout reader for <uiComponent> and one on the generator. Both ask the definition loader the same question: does any enabled module ship view/adminhtml/grid/<name>.json or view/adminhtml/form/<name>.json for this component name? If yes, the component is never scheduled and never rendered. If no, the core reader proceeds and the component renders as stock.

So <uiComponent name="customer_listing"/> is skipped because Nebula ships grid/customer_listing.json, and a third-party <uiComponent name="vendor_thing_listing"/> is read normally. What happens to that third-party component is covered on Troubleshooting and UI Bridge.

3. Grids are mounted by layout XML

A grid page is a layout handle that removes the core block and adds Nebula's:

view/adminhtml/layout/customer_group_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="customer_group_listing" remove="true"/>
        <referenceContainer name="content">
            <block class="Nebula\Grid\Block\Grid" name="nebula.customer_group_listing">
                <arguments>
                    <argument name="grid_id" xsi:type="string">customer_group_listing</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

The remove is belt and braces; the UI Component removal already skipped the component. For legacy grids that are PHP blocks rather than UI Components, Nebula_Grid ships a RemoveLegacyGridBlocks plugin that removes the block by name. The block name in <referenceBlock> is typed by hand, and a typo means the core grid renders beside the Nebula one, so check the page after you mount a grid.

4. Forms mount themselves by route

A form definition declares where it belongs:

{
  "id": "cms_block_edit",
  "route": "cms_block_edit",
  "replaces": "cms_block_form",
  "acl": "Magento_Cms::save"
}
Key Meaning
route One admin full action name, or a list of them. cms_block_edit is the request cms/block/edit. Matched case-insensitively.
replaces Legacy block name or names to remove from the page. Underscore, dot and dash names all work.
replacesClass Legacy block class names, for controllers that create their block with createBlock(Foo::class) and no stable name.
routeWhen A guard, for routes that several forms share.

RouteFormMap discovers every form definition across enabled modules, resolves each one, and builds a map from route to candidates. InjectFormLayout, an after plugin on the layout processor's load(), looks up the current request's full action name and appends this layout update:

<referenceContainer name="page.main.actions" remove="true"/>
<referenceBlock name="cms_block_form" remove="true"/>
<referenceContainer name="content">
    <block class="Nebula\Form\Block\Form" name="nebula.cms_block_edit">
        <arguments>
            <argument name="form_id" xsi:type="string">cms_block_edit</argument>
        </arguments>
    </block>
</referenceContainer>

The core page actions container goes because the form shell has its own sticky action bar. The plugin also adds nebula_form_<id> to the layout cache key, so two forms that mount on one route do not share a cached structure.

Guards: routeWhen

Some full action names are not one page. Core's EditWebsite and EditGroup controllers set a store_type in the registry and then forward to editStore, so three entity types arrive as adminhtml_system_store_editstore. A guard tells them apart:

store_view_edit.json
{
  "id": "store_view_edit",
  "route": "adminhtml_system_store_editstore",
  "routeWhen": { "registry": { "store_type": "store" } },
  "replaces": "adminhtml_admin_save_confirm"
}
url_rewrite_edit.json
{
  "id": "url_rewrite_edit",
  "route": "adminhtml_url_rewrite_edit",
  "routeWhen": { "param": { "product": null, "category": null, "cms_page": null } },
  "replacesClass": "Magento\\UrlRewrite\\Block\\Edit"
}

Two sources, registry and param. Every entry must hold. A scalar means present and equal, compared as strings. null means the key must be absent. true means present with any value. Candidates on one route are ordered most specific first and an unguarded definition is the fallback. Two definitions with the same guard on the same route are a conflict: the second is dropped and a warning is logged.

The evaluator fails closed. An unknown source or a malformed block means "does not match", so the definition does not mount and the core page renders alone, rather than a Nebula form rendering beside a core one.

EAV forms mount by layout

Product, category and customer forms are EAV forms. They do not use route; they are mounted by a layout handle that includes nebula_eav_form. The reason and the mechanics are on EAV forms.

5. DI preferences

A set of core admin blocks is replaced through <preference> in di.xml. The replacement blocks and the plugins that change core behaviour (the configuration form, the report blocks, the menu, the tabbed edit forms, the favicon, and others) ask AdminThemeStateInterface::isNebulaRendering() before they act. The free package answers true always. The theme switcher module replaces that answer with its configuration, which is how every Nebula replacement stands down together when the merchant selects another admin theme.

6. The content pack

Nebula_Theme and the screen modules (NebulaStore, NebulaSystem, NebulaUser, NebulaCurrency, NebulaReports, and the rest) ship the definitions, data providers, layout handles, snippets and view models that make the mechanisms above replace each core screen. Together they cover the catalog, customers, sales, marketing, content, reports, stores and system areas.

Checking a replaced page

Open it, submit it empty and require an error message, do the real thing once and find the record again, and click every toolbar button. A button carrying data-nebula-dropped-onclick had its handler stripped and does nothing. That check is the project's definition of "converted", and it is the check to run on a page after you overlay it.