images()->count(); return $count > 0 ? (string) $count : null; } public function form(Schema $schema): Schema { return $schema->components([ FileUpload::make('path') ->label(__('panel.gallery.fields.image')) ->image() // Explicit: the default disk is private, and the frontend runs // on another host. ->disk(config('media.disk')) ->directory('projects/gallery') ->imageEditor() ->required() ->columnSpanFull(), TranslatableTabs::make(fn (string $locale) => [ TextInput::make("alt.{$locale}") ->label(__('panel.gallery.fields.alt')) ->helperText(__('panel.gallery.fields.alt_hint')) ->maxLength(255) ->columnSpanFull(), // Shown to everyone, unlike the alt text. "Before" and // "after" belong here. TextInput::make("caption.{$locale}") ->label(__('panel.gallery.fields.caption')) ->helperText(__('panel.gallery.fields.caption_hint')) ->maxLength(255) ->columnSpanFull(), ])->columnSpanFull(), ]); } public function table(Table $table): Table { $locale = app()->getLocale(); return $table ->defaultSort('position') ->reorderable('position') // The table shows each image's real dimensions; loaded up front so // that costs one query rather than one per row. ->modifyQueryUsing(fn (Builder $query) => $query->with('media')) ->columns([ ImageColumn::make('path') ->label('') ->disk(config('media.disk')) ->height(56), TextColumn::make('caption') ->label(__('panel.gallery.fields.caption')) ->state(fn (ImageAttachment $record) => $record->getTranslation('caption', $locale, false) ?: null) ->placeholder('—') ->wrap(), TextColumn::make('alt') ->label(__('panel.gallery.fields.alt')) ->state(fn (ImageAttachment $record) => $record->getTranslation('alt', $locale, false) ?: null) // An image nobody described is the one worth spotting from // the listing. ->placeholder(__('panel.gallery.missing_alt')) ->color(fn (ImageAttachment $record) => $record->getTranslation('alt', $locale, false) ? null : 'warning') ->wrap() ->toggleable(), TextColumn::make('media.width') ->label(__('panel.gallery.fields.size')) ->state(fn (ImageAttachment $record) => $record->media?->width ? "{$record->media->width}×{$record->media->height}" : __('panel.gallery.pending')) ->badge() ->color(fn (ImageAttachment $record) => $record->media?->width ? 'gray' : 'warning') ->toggleable(), ]) ->headerActions([ CreateAction::make() ->label(__('panel.gallery.actions.add')) // New images go at the end rather than jumping to the top. ->mutateDataUsing(function (array $data): array { $data['position'] = (int) $this->getOwnerRecord()->images()->max('position') + 1; return $data; }), ]) ->recordActions([ EditAction::make(), DeleteAction::make(), ]) ->toolbarActions([ DeleteBulkAction::make(), ]) ->emptyStateHeading(__('panel.gallery.empty.heading')) ->emptyStateDescription(__('panel.gallery.empty.description')) ->emptyStateIcon('heroicon-o-photo'); } }