Search by

wotz / filament-brigada-cms

Who Owns The Zebra

The conventions, defaults and shared UI behind a Brigada Filament CMS

Package info

github.com/wotzebra/filament-brigada-cms

pkg:composer/wotz/filament-brigada-cms

Statistics

Installs: 22

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.8.2 2026-09-15 13:01 UTC

README

The conventions, defaults and shared UI behind a Brigada Filament CMS — the parts that were previously copied out of a project skeleton and then drifted.

Pulls in wotz/filament-brigada-theme and the standard plugin set, so a project gets a styled, configured panel from one composer require.

Installation

composer require wotz/filament-brigada-cms

Import the stylesheets from your panel theme, with the other @import rules at the top — CSS drops an @import that follows any other rule:

@import '../../../../vendor/wotz/filament-brigada-theme/resources/css/filament-brigada-theme.css';
@import '../../../../vendor/wotz/filament-brigada-cms/resources/css/filament-brigada-cms.css';

Extend the panel provider:

use Wotz\FilamentBrigadaCms\Providers\BrigadaPanelProvider;

class AdminPanelProvider extends BrigadaPanelProvider
{
    protected function id(): string
    {
        return 'admin';
    }
}

Then npm run build.

What you get

Panel — the standard plugin set (theme, shield, activity log, sticky header, menu, settings, media library, SEO, redirects, admin bar, environment indicator, unsaved-changes modal, spotlight), Filament's global search replaced by the ⌘K spotlight over navigation, records and actions, and the middleware stack.

Defaults, all applied through configureUsing, so an explicit call in a resource always wins:

Non-clickable rows cell text stays selectable; records open through row actions
Persisted table state sort, search and column searches, not just filters
Humanised labels Amount including vatAmount including VAT
Collapsed nav groups and a default group label
Date and money formats d M Y H:i:s, EUR

Shared UI — the ⌘K trigger, expand/collapse-all for the sidebar, a panel error page, a loading skeleton, CopyTranslationAction, and the HasReadOnlyBadge concern for resources synced from elsewhere.

Drafts and live previewoddvalue/laravel-drafts and wotz/filament-live-preview are wired up: the plugins are registered, draft previews are disabled inside the panel, and two concerns add the actions to a resource's pages.

use Wotz\FilamentBrigadaCms\Filament\Concerns\HandlesDraftsOnEdit;

class EditPage extends EditRecord
{
    use HandlesDraftsOnEdit;   // Publish, Save to new draft, Version switcher
}

Opting a resource in

The package registers the plugins, but nothing appears until a resource opts in — a panel with the plugins loaded and no traits applied shows no Preview or draft actions at all.

On the model:

use Oddvalue\LaravelDrafts\Concerns\HasDrafts;
use Wotz\FilamentBrigadaCms\Models\Concerns\HandlesTranslatableDrafts;

class Page extends Model
{
    use HandlesTranslatableDrafts {
        HandlesTranslatableDrafts::getDraftableAttributes insteadof HasDrafts;
    }
    use HasDrafts;
}

The insteadof is required, not stylistic: both traits define getDraftableAttributes(), and PHP fatals on the collision without it.

HandlesTranslatableDrafts matters more than it looks: laravel-drafts copies raw attributes, so a spatie/laravel-translatable model loses every locale but the current one the first time a draft is saved. On a model that is not translatable, use HasDrafts alone.

Add the columns with the migration helper the drafts package ships:

Schema::table('pages', fn (Blueprint $table) => $table->drafts());

On the pages:

class EditPage extends EditRecord
{
    use HandlesDraftsOnEdit;
    use HasLivePreviewComponent;   // from wotz/filament-live-preview

    protected function getHeaderActions(): array
    {
        return [
            ...$this->getDraftHeaderActions(),
            // your own actions after these
            DeleteAction::make(),
        ];
    }

    protected function getPreviewModalView(): ?string
    {
        return 'page.show';
    }

    protected function getPreviewModalDataRecordKey(): ?string
    {
        return 'page';
    }
}

The create page is the same shape, with its own helper:

class CreatePage extends CreateRecord
{
    use HandlesDraftsOnCreate;
    use HasLivePreviewComponent;

    protected function getHeaderActions(): array
    {
        return [
            ...$this->getDraftHeaderActions(),
            $this->getCancelFormAction(),
        ];
    }

    // getPreviewModalView() and getPreviewModalDataRecordKey() as above
}

Live preview renders your own front-end view, so it only applies to resources that have one — and it has to be wired on both the create and edit pages, not just edit. If a project has no use for either, drop them:

protected function plugins(): array
{
    $plugins = parent::plugins();
    unset($plugins['live-preview'], $plugins['peek']);

    return $plugins;
}

Onboarding

Four guided journeys ship with the package, describing the panel every Brigada CMS has: publishing a page, images and crops, menus and redirects. New editors find them under Getting started in the user menu.

Journeys are database rows, so they exist on one machine unless they travel:

php artisan onboarding:export   # panel → database/onboarding/
php artisan onboarding:import   # database/onboarding/ → this environment

onboarding:import runs on deploy. It applies the journeys this package ships first, then the project's own database/onboarding/ — so a project adds journeys about the things only it has, and a file sharing a name with a shipped one replaces it outright. Nothing it does touches progress: what people have actually completed belongs to the environment they completed it in.

Everything is addressed by its natural key, so a second run converges rather than duplicating. --prune removes flows that are no longer in the fixtures, --dry-run reports without writing, and --app-only skips the shipped four.

Shield gates the flow, step and condition resources — the plugin's own policies let anybody who can reach the panel rewrite a journey. A project that has named its own policies keeps them.

Tours point at fields by name, and a field inside translatable tabs renders under a locale-prefixed state path. Resolving those needs wallacemartinss/filament-onboarding#16; until it is released, tours on translated forms will find fewer targets than they name.

Overriding

Every method on BrigadaPanelProvider is a seam. Append rather than replace:

protected function plugins(): array
{
    // Keyed, so you can reconfigure one without rebuilding the list.
    $plugins = parent::plugins();
    $plugins['shield']->navigationGroup(NavigationGroup::General);

    return [...$plugins, 'live-preview' => LivePreviewPlugin::make()];
}

protected function userMenuItems(): array
{
    return [...parent::userMenuItems(), Action::make('maintenanceMode')…];
}

Views override the Laravel way — resources/views/vendor/filament-brigada-cms/… wins. Classes can be swapped through the container. And everything above is togglable in config:

php artisan vendor:publish --tag=filament-brigada-cms-config

Add your project's own acronyms there rather than editing the package:

'acronyms' => [..., 'bc', 'btw', 'kkg', 'ogm'],

Panel error page

Point your resources/views/errors/admin.blade.php at the packaged one:

@include('filament-brigada-cms::errors.panel')

Developing this package

Improvements are meant to be made in the package, while working on a real project — not made in the project and cherry-picked back. Point the project at a local checkout:

composer config repositories.brigada-cms path ../filament-brigada-cms
composer require wotz/filament-brigada-cms:@dev

vendor/wotz/filament-brigada-cms is then a symlink to your checkout, so you edit the package directly with the project running against it. Commit and tag in the package; every other project picks it up on composer update.

Remove the path repository before committing the project:

composer config --unset repositories.brigada-cms

Licence

MIT. See LICENSE.md.