Components

Media Gallery Field

Embed the Tagixo media gallery picker inside Primix forms with the SDK field component.

Media Gallery

The Tagixo Primix integration ships an optional Media Gallery subsystem that gives editors a central place to upload, crop, and pick images. It exposes a Primix resource for managing media, a form field and table column for picker UIs, and a modal component reused inside the visual builder canvas.


1. Enabling the Media Gallery

Register the feature on the plugin in your AdminPanelProvider:

use Ccast\TagixoPrimix\TagixoPrimixPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(
            TagixoPrimixPlugin::make()
                ->withMediaGallery()
        );
}

Alternatively, pass configuration as an array:

TagixoPrimixPlugin::make()
    ->withMediaGallery([
        'disk'            => 'public',
        'directory'       => 'media',
        'max_file_size'   => 10240,    // KB
        'allowed_types'   => ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
        'generate_crops'  => true,
        'visibility'      => 'public', // 'public' | 'private'
    ]),

All keys are optional; defaults are documented in the Upload configuration section below.


2. What the Media Gallery Adds

Enabling the feature registers:

  • MediaResource — a full Primix resource at /admin/media. It renders two views:
    • Grid view — thumbnail cards with hover actions (edit, delete, copy URL).
    • Table view — tabular listing with sortable columns (name, disk, size, created at).
  • Upload action — a header action that opens a multi-file uploader supporting drag-and-drop. Files are validated against allowed_types and max_file_size before being stored on the configured disk.
  • Edit modal — lets editors rename the file, update alt text, and set focal-point crops.
  • Delete action — soft-checks for usages before hard-deleting from disk and the tgx_media table.
  • Crops tab — per-media crop definitions stored as JSON in tgx_media.crops. Predefined crop keys (e.g. thumbnail, og) are configurable; editors can also add ad-hoc named crops.

3. MediaGalleryPickerField

Use this field whenever a Primix form needs a media picker:

use Ccast\TagixoPrimix\Forms\Components\MediaGalleryPickerField;

MediaGalleryPickerField::make('hero_image')
    ->label('Hero image')
    ->required()
    ->allowedTypes(['image/jpeg', 'image/png', 'image/webp'])
    ->cropKey('thumbnail')
    ->returnShape('array')   // 'array' (default) | 'url' | 'id'
    ->previewHeight(160),    // px, default 120

Method reference

Method Type Default Description
allowedTypes(array) array<string> all configured types MIME types shown in the picker
cropKey(string) string|null null If set, the stored value uses the named crop URL instead of the original
returnShape(string) 'array'|'url'|'id' 'array' Shape of the stored value
previewHeight(int) int 120 Height in pixels of the thumbnail preview inside the form
disk(string) string|null plugin default Override the storage disk
nullable() Allows clearing the selection
multiple() Enables multi-select; stored value becomes an array of items

Return value shape

When returnShape is 'array' (the default), the stored value is:

{
    "id": 42,
    "url": "https://example.com/storage/media/photo.jpg",
    "crop_url": "https://example.com/storage/media/crops/photo_thumbnail.jpg",
    "alt": "A hero photo",
    "width": 1920,
    "height": 1080,
    "mime_type": "image/jpeg"
}

When returnShape is 'url', only the resolved URL string is stored. When returnShape is 'id', only the integer media ID is stored.

Model cast

For 'array' return shape, cast the column in your model:

protected $casts = [
    'hero_image' => 'array',
];

LiVue event flow

The picker opens a LiVue-powered modal (<tgx-media-gallery-picker-modal>). Selection fires an update:modelValue emit on the LiVue component, which Primix bridges to Livewire via an @update:modelValue listener that calls $set('data.hero_image', $event). No Alpine.js is involved; the entire round-trip stays within the LiVue → Livewire channel.


4. MediaGalleryPickerColumn

Display a thumbnail in Primix tables:

use Ccast\TagixoPrimix\Tables\Columns\MediaGalleryPickerColumn;

MediaGalleryPickerColumn::make('hero_image')
    ->label('Hero')
    ->height(48)      // px, default 40
    ->width(80)       // px, default auto
    ->circular(false) // default false
    ->tooltip(true),  // show alt text on hover, default false

Method reference

Method Type Default Description
height(int) int 40 Thumbnail height in pixels
width(int|null) int|null null (auto) Thumbnail width in pixels
circular(bool) bool false Renders as a circle (avatar-style)
tooltip(bool) bool false Shows the alt value on hover
fallbackIcon(string) string heroicon-o-photo Icon shown when the cell is empty
cropKey(string) string|null null Prefer a named crop URL if present in the stored array

How it reads stored state

The column resolves the display URL by inspecting the stored value:

  • If the value is an array with a crop_url key and cropKey is configured, it uses crop_url.
  • If the value is an array with a url key, it uses url.
  • If the value is a plain string, it treats it as a direct URL.
  • If the value is an integer, it performs a lazy Media::find() lookup.

Caveat for private disks

When the media disk visibility is private, stored URLs in the database are bare paths (no domain). The column calls Media::getUrlAttribute() at render time to generate a signed URL. This adds one query per row unless you eager-load the media relationship or use returnShape('url') with a pre-signed value. See Signed URL generation below.


5. Builder Media Modal

Inside the visual builder canvas, image props use the same media picker infrastructure via the <tgx-media-picker-field> Vue component. This component is the canonical single implementation — it is not a copy.

Component registration: The component is auto-registered by the Tagixo JS bundle and available inside the iframe canvas without any additional import.

Data source: The modal fetches the media library via a REST endpoint (GET /tagixo/api/media) rather than through the Livewire/Primix stack. This keeps the canvas iframe decoupled from the parent Livewire page.

Vue store integration: The selected media object is written directly into the builder's Pinia store (useBuilderStore().setActivePropValue(key, value)). The REST fetch result is cached in a separate useMediaStore() to avoid redundant round-trips while the editor has the modal open.

Prop serialisation: Builder media values follow the same 'array' shape as the Primix field. The PHP renderer (PageRenderer) resolves crop_url when a cropKey is set on the prop definition, falling back to url.


6. Upload Configuration

All keys live under config/tagixo.php in the media_gallery namespace. Set overrides via .env.

Config key Env variable Default Description
media_gallery.disk TAGIXO_MEDIA_DISK 'public' Laravel filesystem disk for uploads
media_gallery.directory TAGIXO_MEDIA_DIR 'media' Sub-directory on the disk
media_gallery.max_file_size TAGIXO_MEDIA_MAX_SIZE 10240 Maximum upload size in KB (10 MB)
media_gallery.allowed_types ['image/jpeg','image/png','image/webp','image/gif','image/svg+xml'] Accepted MIME types (array, not env-configurable)
media_gallery.visibility TAGIXO_MEDIA_VISIBILITY 'public' 'public' or 'private'
media_gallery.signed_url_ttl TAGIXO_MEDIA_URL_TTL 86400 Signed URL TTL in seconds (24 h)
media_gallery.generate_crops TAGIXO_MEDIA_CROPS true Whether to run crop generation on upload
media_gallery.crop_definitions ['thumbnail'=>[300,300],'og'=>[1200,630]] Named crop sizes [width, height]
media_gallery.image_driver TAGIXO_MEDIA_DRIVER 'gd' Intervention Image driver ('gd' or 'imagick')

7. Signed URL Generation

When visibility is set to private, media files are stored without public access. URL resolution goes through Media::getUrlAttribute():

// Simplified internal logic
public function getUrlAttribute(): string
{
    if ($this->visibility === 'private') {
        $ttl = config('tagixo.media_gallery.signed_url_ttl', 86400);
        return Storage::disk($this->disk)
            ->temporaryUrl($this->path, now()->addSeconds($ttl));
    }

    return Storage::disk($this->disk)->url($this->path);
}

TTL: Signed URLs expire after 24 hours by default (TAGIXO_MEDIA_URL_TTL=86400).

HTML caching warning: If you cache rendered page HTML (e.g. via a full-page cache), signed URLs embedded in src attributes will expire before the cache entry does. Either:

  • Use public visibility for images served in HTML, and reserve private for download-only assets.
  • Set the HTML cache TTL shorter than the signed URL TTL.
  • Avoid caching pages that contain private media; add a Cache-Control: no-store header for those routes.

Crop URLs follow the same visibility path — each crop entry in tgx_media.crops stores the bare path, not the URL; the accessor is called per-crop at serialisation time.


8. LiVue-Specific Notes

update:modelValue vs. Livewire wire events

The <tgx-media-gallery-picker-modal> component emits update:modelValue following the standard Vue 3 v-model contract. Primix wraps this in a thin Livewire bridge; do not use wire:model directly on the component — the bridge handles the sync. In custom LiVue pages outside Primix, listen for update:modelValue in your Vue component tree:

<tgx-media-picker-field
    :model-value="heroImage"
    @update:model-value="heroImage = $event"
/>

No Alpine.js dependency

The media picker does not use Alpine.js. Do not add x-data or @click.away hooks around it expecting Alpine to intercept events — LiVue's reactivity system handles all state.

No PHP round-trip on selection

Selecting a media item in the picker does not fire a Livewire request. The media list is fetched once from the REST endpoint and cached in the Vue store for the duration of the page session. A PHP round-trip only occurs when Primix persists the form on save.

Debugging guidance

  • Picker opens but shows no media: Check that the REST endpoint GET /tagixo/api/media returns a 200 with valid JSON. Confirm the authenticated user has the browse-media permission.
  • Selection does not persist: Open Vue DevTools and verify the Pinia useBuilderStore or the Livewire component state updates on update:modelValue. If not, check that the field's wire:key is unique within its repeater context.
  • Crop thumbnails missing: Run php artisan tagixo:regenerate-crops to reprocess existing uploads.

9. Permission Gating

The Media Gallery is gated behind the manage-media permission, which is checked in MediaResource::canAccess().

Seeder

The plugin ships a seeder that registers the permission:

php artisan db:seed --class="Ccast\\TagixoPrimix\\Database\\Seeders\\MediaPermissionsSeeder"

This creates two granular permissions that you can assign independently:

Permission Description
browse-media View the media library and use the picker
upload-media Upload new files (implies browse-media)

manage-media is a convenience aggregate that grants both.

Browse-vs-upload separation pattern

To allow editors to pick from existing media but not upload new files, assign only browse-media:

// In your role seeder or panel AuthServiceProvider
$editorRole->givePermissionTo('browse-media');
$adminRole->givePermissionTo('manage-media');

The MediaResource upload action checks for upload-media before rendering; editors with only browse-media see a read-only gallery. The picker field and column are unaffected by upload permissions — they only require browse-media.