github shopperlabs/shopper v3.0.0-beta.5

pre-release10 hours ago

Caution

This is a beta release of the framework. Breaking changes may be introduced to v3 releases during the beta period.

This fifth 3.0 beta adds bulk product import and reworks how a product is published to sales channels. It also closes a run of admin UX gaps: variant ordering that never wrote its position, product thumbnails that needed a second upload, and customers without a role going invisible on their own orders. The official 3.0 release is coming this September.

Installation

"minimum-stability": "beta",
"prefer-stable": true
composer require shopper/framework:^3.0.0-beta

Highlights

Product import pipeline

Products can now be imported in bulk instead of created one at a time. A new ImportManager singleton resolves pluggable ImportSource implementations, mirroring the ChannelManager driver pattern, so the upcoming migrator addon can register WooCommerce, PrestaShop and other platform sources with ImportManager::extend() without touching core:

interface ImportSource
{
    public function code(): string;

    public function name(): string;

    public function description(): string;

    public function icon(): string;

    public function isConfigured(): bool;

    /**
     * @return LazyCollection<int, ProductRow>
     */
    public function read(string $path): LazyCollection;
}

The built-in CsvSource reads the canonical Shopper product template with league/csv, groups flat rows by handle into a ProductRow DTO (variants, options, images, categories, tags), and accepts a stored column mapping through SupportsColumnMapping so a file with different headers still lines up with Shopper's fields. ProductRowImporter then writes each product idempotently (matched by slug, variants by SKU, brands and categories resolved or created by name), and downloads images in separate queued jobs so one dead URL never fails a whole product. StartProductImport reads the source once, splits the rows into Bus::batch() chunks of shopper.core.import.chunk_size rows (25 by default), tracks progress on a new ProductImport model, and notifies administrators with the final counts when the batch completes. In the admin, Products > Import opens a three step wizard: upload, column mapping with auto-matched headers, then a review step with parsed totals.

Product overview redesign and sales channel toggles

The product overview page replaces the sales channels multi-select in Associations with a dedicated toggle list: one row per channel showing its driver logo through ChannelManager::logoFor(), the channel name, and a native Filament toggle, with a live "Available in X of Y sales channels" counter.

class ChannelToggles extends CheckboxList
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->disableOptionWhen(
            fn (string $value): bool => ! $this->getChannels()->firstWhere('id', (int) $value)?->is_enabled
        );
    }

    /**
     * Disabled channels stay valid so existing attachments survive a save.
     */
    public function getInValidationRuleValues(): ?array
    {
        return $this->getChannels()->pluck('id')->map(fn ($id): string => (string) $id)->all();
    }
}

Filament's CheckboxList validates against enabled options only by default, which would silently drop a product's channel the moment that channel gets disabled. ChannelToggles overrides getInValidationRuleValues() to keep every channel id valid, so a disabled channel shows a locked toggle but keeps its existing attachments on save. The products table also gained a sales channel filter.

New Features

  • feat(i18n): add Swedish translations by @adevade in #633
  • feat(core): product import pipeline with CSV source and column mapping by @mckenziearts in #629
  • feat(admin): redesign the product overview page with sales channel toggles by @mckenziearts in #628
  • feat(admin): product thumbnail gallery fallback and use-as-thumbnail picker by @mckenziearts in #627
  • feat(admin): variant reordering, product page titles and breadcrumbs by @mckenziearts in #625

Bug Fixes

Upgrading

Run your migrations. This release adds a product_imports table that tracks import progress and counts:

php artisan migrate

Published product.php component config. mergeConfigFrom() merges shallowly at the components key, so a config/shopper/components/product.php published before this release (php artisan shopper:component:publish product) keeps its own stale components array and drops the two entries the new import feature needs. Add them by hand, or republish the file:

'components' => [
    // ...
    'products.import-progress' => Components\Products\ImportProgress::class,
    'slide-overs.import-csv' => Livewire\SlideOvers\ImportCsv::class,
],

Without them, opening Products > Import throws Livewire\Exceptions\ComponentNotFoundException.

Behavior changes to review

  • Variants now have a real default order. Product::variants() is ordered by position instead of an unspecified row order. Existing variants were never assigned one and all sit at 1, so nothing visibly changes until a merchant drags a variant or a new one is created, which now appends at the end.
  • The customers scope now includes roleless users. User::customers() used to require the user role exactly. It now returns anyone who does not hold the admin or manager role, plus anyone holding the user role, so a buyer created without a role shows up in Customers and on their order instead of 404ing. New orders now assign the user role to their customer automatically.
  • The Store API thumbnail field now falls back to the first gallery image. Products and variants without a dedicated thumbnail used to serialize thumbnail: null; they now return the first gallery image, matching what the admin already displayed. Storefronts checking for "no image" should check the images array instead of thumbnail === null.

Contributors

Full Changelog: v3.0.0-beta.4...v3.0.0-beta.5

Don't miss a new shopper release

NewReleases is sending notifications on new releases.