Skip to content

Contributing Tiles & Cards ​

Ship a module. Months later, ship a second module that has something to say about the same business object. The second module's tile appears on the first module's dashboard — and you never touch the first module. The dashboard grew because the ecosystem grew.

This is LUX Weave applied to dashboards: name a stable anchor, contribute to it, and the receiving side stays untouched. Where Weave's links anchor on a concept, a dashboard contribution anchors on a dashboard group's namespace.

In one sentence. A tile or card marks itself #[Contribute('<target group namespace>')]; the SDK drops it into that group, and the consuming dashboard renders it with no edit to the consuming app — Core's composition, the visibility veto chain, and per-tile error isolation apply to it like to any other tile.


The two roles ​

Cross-module dashboard composition has two sides, and only one of them writes code:

RoleWhoWhat they do
Contributorthe module that has a tile to addmarks the tile #[Contribute(<groupNs>)] and registers it on its own module
Consumerthe module that owns the dashboardnothing — every dashboard group is composable already

There is no "make my dashboard extensible" step. The consuming group is a plain, static declaration; it does not know its contributors and is never edited to receive them.


Contributing a tile ​

Put the #[Contribute] attribute on any Tile or Card, naming the published namespace of the target dashboard group, and register the tile on your own module as usual:

php
namespace App\Billing\Tiles;

use LaravelUi5\Core\Ui5\AbstractUi5Tile;
use LaravelUi5\Sdk\Weave\Dashboard\Attributes\Contribute;

#[Contribute('com.laravelui5.partners.groups.registry-kpi', weight: 50)]
class OutstandingInvoicesTile extends AbstractUi5Tile
{
    public const NAMESPACE = 'app.billing.tiles.outstanding_invoices';
    // … VERSION, TITLE, getTileProvider() …
}
php
// App\Billing\BillingModule
public function getTiles(): array
{
    return [
        new Tiles\OutstandingInvoicesTile($this),
    ];
}

That is the whole contract. The tile is a normal artifact on the Billing module — its provider renders it — and the #[Contribute] marker routes it into the Partners registry-KPI group. Note that nothing on the dashboard reads the tile's #[Access]: see Visibility for who sees it.

The anchor is a namespace string ​

The first argument is the target group's published namespace — the value of the group's NAMESPACE constant. Pass it as a string literal, not a class import: the contributor depends only on the other module's published surface, never on its PHP classes. A group's namespace is a stable, published identifier, exactly like a Concept name or an app namespace.

Find the namespace in the owning module's documentation (for the Partners Overview it is com.laravelui5.partners.groups.registry-kpi, exposed as RegistryKpiGroup::NAMESPACE).


Nothing to wire on the consuming side ​

Core ships a DashboardGroupCollector that the dashboard transformer already merges into every group's children. The SDK reads your #[Contribute] markers and feeds that collector for you the first time a dashboard renders. So the consuming group stays a plain list of its own tiles:

php
class RegistryKpiGroup extends AbstractUi5DashboardGroup
{
    public const NAMESPACE = 'com.laravelui5.partners.groups.registry-kpi';

    public function getChildNamespaces(): array
    {
        return [
            TotalPartnersTile::NAMESPACE,
            OrganizationsTile::NAMESPACE,
            PersonsTile::NAMESPACE,
        ];
        // Contributions are merged in by the transformer — not listed here.
    }
}

No harvest command, no registry entry, no migration. Deploy the contributing module, and the tile is there on the next request.


Order, visibility, and errors ​

Contributed children travel the same composition path as a group's own children:

  • Order. Contributions render after the group's own tiles. Among themselves they sort by weight ascending (small = high priority), then by namespace. weight defaults to 0.
  • Visibility. A contributed tile passes through the same veto chain as every other tile, before any data is fetched. Today nothing in that chain reads a tile's #[Access]: the only vetoer the SDK registers is the Launchpad's, and it prunes Launchpad launcher tiles only. A contributed tile therefore renders for everyone who can open the dashboard, unless you register a vetoer of your own (below).
  • Errors. If a contributed tile's provider throws, that tile is dropped and its siblings still render (per-tile error isolation). One module's bug cannot break another module's dashboard.
  • Lifecycle. Remove the contributing module and its tile simply disappears — the contribution is read live from what is installed, so there are never orphaned doorways.

Hiding a contributed tile ​

To hide a tile from some viewers, implement Core's VetoerInterface and tag it into the veto chain from a service provider. Vetoers compose by union: any Hide wins.

php
use LaravelUi5\Core\Ui5\Contracts\Ui5ArtifactInterface;
use LaravelUi5\Core\Ui5\Contracts\Ui5ContextInterface;
use LaravelUi5\Core\Ui5\Emit\Veto\Disposition;
use LaravelUi5\Core\Ui5\Emit\Veto\VetoerInterface;

final class BillingTileVetoer implements VetoerInterface
{
    public function dispose(Ui5ArtifactInterface $artifact, Ui5ContextInterface $context): Disposition
    {
        if ($artifact->getNamespace() !== OutstandingInvoicesTile::NAMESPACE) {
            return Disposition::Show;
        }

        // Your rule, decided from the context (actor, abilities, …).
        return $this->allows($context) ? Disposition::Show : Disposition::Hide;
    }
}

// in a service provider
$this->app->tag([BillingTileVetoer::class], VetoerInterface::class);

A group whose children are all pruned still renders, as an empty panel. The veto chain votes on groups too, so the same vetoer can hide the group itself.


Worked example — settings overrides on the Partners Overview ​

The SettingsApp ships a single tile:

php
#[Contribute('com.laravelui5.partners.groups.registry-kpi', weight: 50)]
class PartnerSettingsOverridesTile extends AbstractUi5Tile { /* … */ }

Its provider counts the settings that have been overridden above the platform default and renders the number. The tile lives entirely on the SettingsApp; the Partners app is not edited. Open the Partners Overview and the KPI region now carries a "Settings Overrides" tile next to the partner counts — a panel that grew because a second app shipped, not because the Partners app changed. No vetoer covers it, so every viewer of the Partners Overview sees it.


Scope today ​

  • Applies to Tiles and Cards (Ui5TileInterface / Ui5CardInterface).
  • One target group per tile (the attribute is not repeatable).
  • weight gives a stable order; a richer ordering / de-duplication / per-user personalization contract is intentionally deferred.
  • No #[Access]-based pruning ships yet; who sees a contributed tile is decided by a vetoer you register.

See also ​

  • LUX Weave: the concept graph and the other contribution facets
  • Value Helps: contributing a scope to another vendor's picker
  • The Launchpad: the shipped dashboard every module contributes its launcher to
  • The Settings App: the app behind the worked example above