From eaf40c471638075f1b807824bbc504860e4405d8 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 1 Sep 2025 12:50:13 -0700 Subject: [PATCH 1/6] Wip --- src/Livewire/MediaUploader.php | 96 +++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/src/Livewire/MediaUploader.php b/src/Livewire/MediaUploader.php index 4a945c5..e65690a 100644 --- a/src/Livewire/MediaUploader.php +++ b/src/Livewire/MediaUploader.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Str; use Livewire\Attributes\Locked; +use Livewire\Attributes\On; use Livewire\Component; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; use Livewire\WithFileUploads; @@ -40,9 +41,13 @@ class MediaUploader extends Component public ?int $confirmingDeleteId = null; public string $allowedLabel = ''; public ?string $theme = null; + public ?string $pendingModelClass = null; // new + public ?string $channel = null; - #[Locked] public string $resolvedModelClass; - #[Locked] public int|string $resolvedModelId; + #[Locked] + public ?string $resolvedModelClass = null; + #[Locked] + public int|string|null $resolvedModelId = null; public function mount( $for = null, @@ -57,10 +62,12 @@ public function mount( array $namespaces = null, array $aliases = null, string $attachedFilesTitle = "Attached media", + ?string $channel = null, ): void { if ($namespaces !== null) $this->namespaces = $namespaces; if ($aliases !== null) $this->aliases = $aliases; + $this->channel = $channel; $this->collection = $collection ?: 'images'; $this->disk = $disk; $this->multiple = $multiple; @@ -74,22 +81,28 @@ public function mount( if ($for instanceof Model) { if (! $for->exists) abort(422, 'Target model must be saved before attaching media.'); if (! $for instanceof HasMedia) abort(422, class_basename($for) . ' must implement Spatie\\MediaLibrary\\HasMedia.'); - $this->resolvedModelClass = $for::class; $this->resolvedModelId = (string) $for->getKey(); - } else { - if (! $model || $id === null) abort(422, 'Provide either :for="$model" or model + id.'); - + } elseif ($model) { $fqcn = $this->resolveModelClass($model); if (! in_array(HasMedia::class, class_implements($fqcn), true)) { abort(422, class_basename($fqcn) . ' must implement Spatie\\MediaLibrary\\HasMedia.'); } - $fqcn::findOrFail($id); - $this->resolvedModelClass = $fqcn; - $this->resolvedModelId = (string) $id; + if ($id !== null) { + $fqcn::findOrFail($id); + $this->resolvedModelClass = $fqcn; + $this->resolvedModelId = (string) $id; + } else { + // PENDING: we only know the class for now + $this->pendingModelClass = $fqcn; + } + } else { + abort(422, 'Provide either :for="$model" or model="Class" (id optional).'); } - if ($this->showList) $this->loadItems(); + if ($this->showList && $this->hasTarget()) { + $this->loadItems(); + } } protected function metaRules(int $mediaId): array @@ -200,8 +213,16 @@ protected function resolveModelClass(string $value): string abort(422, "Unknown model class/alias [{$value}]."); } - protected function target(): Model + protected function hasTarget(): bool + { + return !empty($this->resolvedModelClass) + && $this->resolvedModelId !== null + && $this->resolvedModelId !== ''; + } + + protected function target(): ?Model { + if (! $this->hasTarget()) return null; $cls = $this->resolvedModelClass; return $cls::findOrFail($this->resolvedModelId); } @@ -256,9 +277,14 @@ public function uploadFiles(): void 'uploads.*' => $perFileRules, ] + $this->queueMetaRules()); + if (! $this->hasTarget()) { + session()->flash('media_uploader_notice', 'Files queued. They will be attached after you save.'); + return; + } + $model = $this->target(); $collection = $this->collection ?? 'default'; - $added = $replaced = $skipped = $renamed = 0; + $added = $replaced = $skipped = $renamed = 0; foreach ($this->uploads as $i => $file) { $originalName = method_exists($file, 'getClientOriginalName') @@ -321,6 +347,47 @@ public function uploadFiles(): void session()->flash('media_uploader_notice', $msg); } + #[On('media:attach')] + public function attachTo( + string $model, + int|string $id, + ?string $collection = null, + ?string $disk = null, + ?string $channel = null // optional scoping + ): void { + // If you use channels, ignore events for other uploaders + if ($this->channel && $this->channel !== $channel) return; + + $fqcn = $this->resolveModelClass($model); + if (! in_array(HasMedia::class, class_implements($fqcn), true)) { + abort(422, class_basename($fqcn) . ' must implement Spatie\\MediaLibrary\\HasMedia.'); + } + $fqcn::findOrFail($id); + + $this->resolvedModelClass = $fqcn; + $this->resolvedModelId = (string) $id; + + $originalCollection = $this->collection; + $originalDisk = $this->disk; + if ($collection) $this->collection = $collection; + if ($disk) $this->disk = $disk; + + if (empty($this->uploads)) { + // Nothing queued; just let the parent know we’re ready + $this->dispatch('media-attached', model: $fqcn, id: (string) $id); + $this->collection = $originalCollection; + $this->disk = $originalDisk; + return; + } + + $this->uploadFiles(); // will now succeed because target is set + + $this->collection = $originalCollection; + $this->disk = $originalDisk; + + $this->dispatch('media-attached', model: $fqcn, id: (string) $id); + } + public function remove(int $mediaId): void { $media = Media::findOrFail($mediaId); @@ -360,6 +427,11 @@ protected function isImageLike(mixed $file): bool public function loadItems(): void { + if (! $this->hasTarget()) { + $this->items = []; + return; + } + $model = $this->target(); $collection = $this->collection ?? 'default'; From 034eaf078e05f262c95de3ae051f7546a1e73558 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 1 Sep 2025 13:17:32 -0700 Subject: [PATCH 2/6] Wip --- src/Livewire/MediaUploader.php | 56 +++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Livewire/MediaUploader.php b/src/Livewire/MediaUploader.php index e65690a..8123bb2 100644 --- a/src/Livewire/MediaUploader.php +++ b/src/Livewire/MediaUploader.php @@ -125,14 +125,24 @@ protected function queueMetaRules(): array protected function nextOrder(): int { - $model = $this->target(); + // While creating: base order from queued items only + if (! $this->hasTarget()) { + $maxPending = (int) collect($this->pendingMeta ?? [])->pluck('order')->max(); + return ($maxPending ?: 0) + 1; + } + + // Editing an existing model: read current max order from DB + $model = $this->target(); // now non-null $collection = $this->collection ?? 'default'; - return (int) ($model->media() - ->where('collection_name', $collection) - ->max('order_column') ?? 0) + 1; + $max = (int) ($model->media() + ->where('collection_name', $collection) + ->max('order_column') ?? 0); + + return $max + 1; } + protected function csvToArray(?string $csv): array { return collect(explode(',', (string) $csv)) @@ -215,18 +225,24 @@ protected function resolveModelClass(string $value): string protected function hasTarget(): bool { - return !empty($this->resolvedModelClass) + // these are typed but may be uninitialized; isset() is safe + return isset($this->resolvedModelClass, $this->resolvedModelId) + && $this->resolvedModelClass !== '' && $this->resolvedModelId !== null && $this->resolvedModelId !== ''; } + + // change return type to ?Model protected function target(): ?Model { if (! $this->hasTarget()) return null; + $cls = $this->resolvedModelClass; return $cls::findOrFail($this->resolvedModelId); } + public function updatedUploads(): void { $list = is_array($this->uploads) ? $this->uploads : []; @@ -282,7 +298,7 @@ public function uploadFiles(): void return; } - $model = $this->target(); + $model = $this->target(); $collection = $this->collection ?? 'default'; $added = $replaced = $skipped = $renamed = 0; @@ -353,10 +369,11 @@ public function attachTo( int|string $id, ?string $collection = null, ?string $disk = null, - ?string $channel = null // optional scoping + ?string $channel = null, // <— NEW ): void { - // If you use channels, ignore events for other uploaders - if ($this->channel && $this->channel !== $channel) return; + if ($this->channel && $channel && $channel !== $this->channel) { + return; + } $fqcn = $this->resolveModelClass($model); if (! in_array(HasMedia::class, class_implements($fqcn), true)) { @@ -367,27 +384,22 @@ public function attachTo( $this->resolvedModelClass = $fqcn; $this->resolvedModelId = (string) $id; - $originalCollection = $this->collection; - $originalDisk = $this->disk; + $origCollection = $this->collection; + $origDisk = $this->disk; if ($collection) $this->collection = $collection; if ($disk) $this->disk = $disk; - if (empty($this->uploads)) { - // Nothing queued; just let the parent know we’re ready - $this->dispatch('media-attached', model: $fqcn, id: (string) $id); - $this->collection = $originalCollection; - $this->disk = $originalDisk; - return; + if (!empty($this->uploads)) { + $this->uploadFiles(); } - $this->uploadFiles(); // will now succeed because target is set + $this->collection = $origCollection; + $this->disk = $origDisk; - $this->collection = $originalCollection; - $this->disk = $originalDisk; - - $this->dispatch('media-attached', model: $fqcn, id: (string) $id); + $this->dispatch('media-attached', model: $fqcn, id: (string) $id, channel: $channel); } + public function remove(int $mediaId): void { $media = Media::findOrFail($mediaId); From 4f44a94377c3554da087b5092f9c91475f50749c Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 1 Sep 2025 13:33:50 -0700 Subject: [PATCH 3/6] Wip --- .../themes/bootstrap/media-uploader.blade.php | 326 ++++++++++++------ src/Livewire/MediaUploader.php | 65 ++-- 2 files changed, 255 insertions(+), 136 deletions(-) diff --git a/resources/views/themes/bootstrap/media-uploader.blade.php b/resources/views/themes/bootstrap/media-uploader.blade.php index b8aa591..a694014 100644 --- a/resources/views/themes/bootstrap/media-uploader.blade.php +++ b/resources/views/themes/bootstrap/media-uploader.blade.php @@ -256,144 +256,242 @@ class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center gap-2"

No gallery images yet.

@else -
    - @foreach ($items as $m) - @php - $id = (int) $m['id']; - $isEditing = isset($editing[$id]); - $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); - @endphp - -
  • - {{-- Thumbnail / icon --}} - @if(!empty($m['thumb'])) - - @else -
    - - - - -
    - @endif + @if (count($items) === 0) +
    +

    No gallery images yet.

    +
    + @else + @if ($listAll) + {{-- Grouped by collection --}} + @foreach ($groups as $collectionName => $collectionItems) +
    +
    {{ $collectionName }}
    +
    - {{-- Content --}} -
    - @if (! $isEditing) +
      + @foreach ($collectionItems as $m) @php - $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); + $id = (int) $m['id']; + $isEditing = isset($editing[$id]); + $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); @endphp -
      - @if ($isImage) - + {{-- Paste your existing
    • ...
    • markup here, unchanged, using $m --}} +
    • + {{-- Thumbnail / icon --}} + @if(!empty($m['thumb'])) + @else - - {{ $linkText }} - +
      + + + + +
      @endif -
    • - @if(!empty($m['description'])) -
      {{ $m['description'] }}
      - @endif -
      {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB
      - @else -
      -
      -
      Caption
      - - @error('editing.'.$id.'.caption') -
      {{ $message }}
      - @enderror + + {{-- Content (unchanged) --}} +
      + @php $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); @endphp + @if (! $isEditing) +
      + @if ($isImage) + + @else + + {{ $linkText }} + + @endif +
      + @if(!empty($m['description'])) +
      {{ $m['description'] }}
      + @endif +
      + {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB + + {{ $m['collection'] }} +
      + @else + {{-- your existing edit form for caption/description/order --}} +
      +
      +
      Caption
      + + @error('editing.'.$id.'.caption')
      {{ $message }}
      @enderror +
      +
      +
      Description
      + + @error('editing.'.$id.'.description')
      {{ $message }}
      @enderror +
      +
      +
      Order
      +
      + +
      + @error('editing.'.$id.'.order')
      {{ $message }}
      @enderror +
      +
      + @endif
      -
      -
      Description
      - - @error('editing.'.$id.'.description') -
      {{ $message }}
      - @enderror +
      + @if (! $isEditing) + + + @else +
      + + +
      + @endif
      + + @endforeach +
    + @endforeach + @else +
      + @foreach ($items as $m) + @php + $id = (int) $m['id']; + $isEditing = isset($editing[$id]); + $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); + @endphp + +
    • + {{-- Thumbnail / icon --}} + @if(!empty($m['thumb'])) + + @else +
      + + + + +
      + @endif + + {{-- Content --}} +
      + @if (! $isEditing) + @php + $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); + @endphp + +
      + @if ($isImage) + + @else + + {{ $linkText }} + + @endif +
      + @if(!empty($m['description'])) +
      {{ $m['description'] }}
      + @endif +
      {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB
      + @else +
      +
      +
      Caption
      + + @error('editing.'.$id.'.caption') +
      {{ $message }}
      + @enderror +
      -
      -
      Order
      -
      +
      +
      Description
      + @error('editing.'.$id.'.description') +
      {{ $message }}
      + @enderror +
      + +
      +
      Order
      +
      + +
      + @error('editing.'.$id.'.order') +
      {{ $message }}
      + @enderror
      - @error('editing.'.$id.'.order') -
      {{ $message }}
      - @enderror
      -
      - @endif -
      + @endif +
      -
      - @if (! $isEditing) - - - @else -
      +
      + @if (! $isEditing) -
      - @endif -
      -
    • - @endforeach -
    + @else +
    + + +
    + @endif +
    +
  • + @endforeach +
+ @endif + @endif -
namespaces = $namespaces; if ($aliases !== null) $this->aliases = $aliases; @@ -75,6 +78,7 @@ public function mount( $this->showList = $showList; $this->maxSizeKb = $maxSizeKb; $this->attachedFilesTitle = $attachedFilesTitle; + $this->listAll = $listAll; $this->loadPresetFromConfig(); @@ -440,35 +444,52 @@ protected function isImageLike(mixed $file): bool public function loadItems(): void { if (! $this->hasTarget()) { - $this->items = []; + $this->items = []; + $this->groups = []; return; } - $model = $this->target(); + $model = $this->target(); $collection = $this->collection ?? 'default'; - $this->items = $model->media() - ->where('collection_name', $collection) - ->orderBy('order_column') - ->get() - ->map(function (Media $m) { - $thumb = $m->hasGeneratedConversion('thumb') ? $m->getUrl('thumb') : $m->getUrl(); - return [ - 'id' => $m->id, - 'file_name' => $m->file_name, - 'name' => $m->name, - 'url' => $m->getUrl(), - 'thumb' => $thumb, - 'size' => $m->size, - 'mime' => $m->mime_type, - 'created' => $m->created_at?->toDateTimeString(), - 'caption' => $m->getCustomProperty('caption'), - 'description' => $m->getCustomProperty('description'), - 'order' => (int) $m->order_column, - ]; - })->toArray(); + $query = $model->media()->orderBy('order_column')->orderBy('id'); + + if (! $this->listAll) { + $query->where('collection_name', $collection); + } else { + $query->orderBy('collection_name'); + } + + $media = $query->get(); + + $flat = $media->map(function (Media $m) { + $thumb = $m->hasGeneratedConversion('thumb') ? $m->getUrl('thumb') : $m->getUrl(); + return [ + 'id' => $m->id, + 'file_name' => $m->file_name, + 'name' => $m->name, + 'url' => $m->getUrl(), + 'thumb' => $thumb, + 'size' => $m->size, + 'mime' => $m->mime_type, + 'created' => $m->created_at?->toDateTimeString(), + 'caption' => $m->getCustomProperty('caption'), + 'description' => $m->getCustomProperty('description'), + 'order' => (int) $m->order_column, + 'collection' => $m->collection_name, // <— important for grouping + 'is_image' => str_starts_with((string) $m->mime_type, 'image/'), + ]; + })->values()->all(); + + $this->items = $flat; + + $this->groups = collect($flat) + ->groupBy('collection') + ->map(fn ($c) => $c->values()->all()) + ->toArray(); } + public function startEdit(int $mediaId): void { $item = collect($this->items)->firstWhere('id', $mediaId); From 9839a2b83a43682b37124b4fea360a23baa0a6b2 Mon Sep 17 00:00:00 2001 From: RJ Date: Mon, 1 Sep 2025 13:44:55 -0700 Subject: [PATCH 4/6] Wip --- .../themes/tailwind/media-uploader.blade.php | 446 ++++++++++++------ 1 file changed, 304 insertions(+), 142 deletions(-) diff --git a/resources/views/themes/tailwind/media-uploader.blade.php b/resources/views/themes/tailwind/media-uploader.blade.php index 098a7aa..37bf82d 100644 --- a/resources/views/themes/tailwind/media-uploader.blade.php +++ b/resources/views/themes/tailwind/media-uploader.blade.php @@ -283,161 +283,332 @@ class="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium

No gallery images yet.

@else -
    - @foreach ($items as $m) - @php - $id = (int) $m['id']; - $isEditing = isset($editing[$id]); - $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); - @endphp + @if (!empty($listAll) && $listAll && !empty($groups)) + {{-- GROUPED BY COLLECTION --}} + @foreach ($groups as $collectionName => $collectionItems) + {{-- Group header --}} +
    +
    + {{ $collectionName }} +
    +
    -
  • - {{-- Thumbnail / icon --}} - @if(!empty($m['thumb'])) - - @else -
    - - - - -
    - @endif +
      + @foreach ($collectionItems as $m) + @php + $id = (int) $m['id']; + $isEditing = isset($editing[$id]); + $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); + $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); + @endphp + +
    • + {{-- Thumbnail / icon --}} + @if(!empty($m['thumb'])) + + @else +
      + + + + +
      + @endif + + {{-- Content --}} +
      + @if (! $isEditing) +
      + @if ($isImage) + + @else + + {{ $linkText }} + + @endif +
      - {{-- Content --}} -
      - @if (! $isEditing) - @php - $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); - @endphp + {{-- Optional description --}} + @if(!empty($m['description'])) +
      {{ $m['description'] }}
      + @endif + + {{-- Size + collection name meta --}} +
      + {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB + @if(!empty($m['collection'])) + + {{ $m['collection'] }} + @endif +
      + @else +
      +
      +
      Caption
      + + @error('editing.'.$id.'.caption') +

      {{ $message }}

      + @enderror +
      + +
      +
      Description
      + + @error('editing.'.$id.'.description') +

      {{ $message }}

      + @enderror +
      + +
      +
      Order
      +
      + +
      + @error('editing.'.$id.'.order') +

      {{ $message }}

      + @enderror +
      +
      + @endif +
      -
      - @if ($isImage) +
      + @if (! $isEditing) - @else - - {{ $linkText }} - + Delete + + @else +
      + + +
      @endif
      - @if(!empty($m['description'])) -
      {{ $m['description'] }}
      - @endif -
      {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB
      +
    • + @endforeach +
    + @endforeach + @else + {{-- SINGLE COLLECTION (original rendering) --}} +
      + @foreach ($items as $m) + @php + $id = (int) $m['id']; + $isEditing = isset($editing[$id]); + $linkText = $m['caption'] ?: ($m['name'] ?: ($m['original_name'] ?? $m['file_name'])); + $isImage = \Illuminate\Support\Str::startsWith($m['mime'] ?? '', 'image/'); + @endphp + +
    • + {{-- Thumbnail / icon --}} + @if(!empty($m['thumb'])) + @else -
      -
      -
      Caption
      - - @error('editing.'.$id.'.caption') -

      {{ $message }}

      - @enderror -
      +
      + + + + +
      + @endif -
      -
      Description
      - - @error('editing.'.$id.'.description') -

      {{ $message }}

      - @enderror + {{-- Content --}} +
      + @if (! $isEditing) +
      + @if ($isImage) + + @else + + {{ $linkText }} + + @endif
      + @if(!empty($m['description'])) +
      {{ $m['description'] }}
      + @endif +
      {{ number_format(($m['size'] ?? 0)/1024, 1) }} KB
      + @else +
      +
      +
      Caption
      + + @error('editing.'.$id.'.caption') +

      {{ $message }}

      + @enderror +
      -
      -
      Order
      -
      +
      +
      Description
      + @error('editing.'.$id.'.description') +

      {{ $message }}

      + @enderror +
      + +
      +
      Order
      +
      + +
      + @error('editing.'.$id.'.order') +

      {{ $message }}

      + @enderror
      - @error('editing.'.$id.'.order') -

      {{ $message }}

      - @enderror
      -
      - @endif -
      + @endif +
      -
      - @if (! $isEditing) - - - @else -
      +
      + @if (! $isEditing) -
      - @endif -
      -
    • - @endforeach -
    + @else +
    + + +
    + @endif + +
  • + @endforeach +
+ @endif - +