feat/docker-compose-update (#18)
Co-authored-by: jon brookes <marshyon@gmail.com> Reviewed-on: https://codeberg.org/headshed/share-lt/pulls/18
This commit is contained in:
parent
fd43495e2d
commit
1a22fd156d
70 changed files with 1068 additions and 745 deletions
50
app/Filament/Resources/Assets/AssetResource.php
Normal file
50
app/Filament/Resources/Assets/AssetResource.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets;
|
||||
|
||||
use App\Filament\Resources\Assets\Pages\CreateAsset;
|
||||
use App\Filament\Resources\Assets\Pages\EditAsset;
|
||||
use App\Filament\Resources\Assets\Pages\ListAssets;
|
||||
use App\Filament\Resources\Assets\Schemas\AssetForm;
|
||||
use App\Filament\Resources\Assets\Tables\AssetsTable;
|
||||
use App\Models\Asset;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AssetResource extends Resource
|
||||
{
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $model = Asset::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::ArrowUpTray;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return AssetForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return AssetsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAssets::route('/'),
|
||||
'create' => CreateAsset::route('/create'),
|
||||
'edit' => EditAsset::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Assets/Pages/CreateAsset.php
Normal file
11
app/Filament/Resources/Assets/Pages/CreateAsset.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAsset extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Assets/Pages/EditAsset.php
Normal file
19
app/Filament/Resources/Assets/Pages/EditAsset.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditAsset extends EditRecord
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Assets/Pages/ListAssets.php
Normal file
19
app/Filament/Resources/Assets/Pages/ListAssets.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Pages;
|
||||
|
||||
use App\Filament\Resources\Assets\AssetResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAssets extends ListRecords
|
||||
{
|
||||
protected static string $resource = AssetResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Filament/Resources/Assets/Schemas/AssetForm.php
Normal file
24
app/Filament/Resources/Assets/Schemas/AssetForm.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Schemas;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class AssetForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('alt_text'),
|
||||
SpatieMediaLibraryFileUpload::make('image')
|
||||
->collection('default')
|
||||
->image()
|
||||
->disk('s3')
|
||||
->visibility('public')
|
||||
->label('Upload Image'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
40
app/Filament/Resources/Assets/Tables/AssetsTable.php
Normal file
40
app/Filament/Resources/Assets/Tables/AssetsTable.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Assets\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AssetsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('alt_text')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ class EntryForm
|
|||
->visible(fn($get) => $get('type') === 'article')
|
||||
->columnSpanFull(),
|
||||
SpatieMediaLibraryFileUpload::make('featured_image')
|
||||
->multiple() // <- force array handling for Filament v4 bug
|
||||
->visible(
|
||||
fn($get) =>
|
||||
$get('type') === 'article' || $get('type') === 'image'
|
||||
|
|
@ -64,9 +65,63 @@ class EntryForm
|
|||
->columnSpanFull()
|
||||
->dehydrated(false)
|
||||
->saveUploadedFileUsing(function ($file, $record) {
|
||||
$diskName = config('media-library.disk_name', 'public');
|
||||
if (is_array($file)) {
|
||||
$file = reset($file);
|
||||
}
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
// Validate upload object early
|
||||
if (
|
||||
! is_object($file) ||
|
||||
! (method_exists($file, 'getRealPath') || method_exists($file, 'getPathname') || method_exists($file, 'getStream') || method_exists($file, 'store'))
|
||||
) {
|
||||
Log::error('Invalid upload object', ['type' => gettype($file)]);
|
||||
throw new \Exception('Invalid upload object provided to saveUploadedFileUsing');
|
||||
}
|
||||
|
||||
// Use safe variables for further calls
|
||||
$realPath = method_exists($file, 'getRealPath') ? $file->getRealPath() : null;
|
||||
$exists = $realPath ? file_exists($realPath) : false;
|
||||
$name = method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : null;
|
||||
|
||||
|
||||
|
||||
|
||||
Log::info('TemporaryUploadedFile Debug', [
|
||||
'path' => $file->getRealPath(),
|
||||
'exists' => file_exists($file->getRealPath()),
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'disk_root' => config('filesystems.disks.local.root'),
|
||||
'is_readable' => is_readable($file->getRealPath()),
|
||||
'is_writable' => is_writable($file->getRealPath()),
|
||||
]);
|
||||
|
||||
// Additional debug: Check if the file is being moved to livewire-tmp
|
||||
$livewireTmpPath = storage_path('framework/livewire-tmp');
|
||||
Log::info('Livewire Temp Directory Debug', [
|
||||
'livewire_tmp_path' => $livewireTmpPath,
|
||||
'exists' => file_exists($livewireTmpPath),
|
||||
'is_writable' => is_writable($livewireTmpPath),
|
||||
]);
|
||||
|
||||
// Check if the file is being moved
|
||||
$tempFilePath = $file->getRealPath();
|
||||
$newFilePath = $livewireTmpPath . '/' . $file->getClientOriginalName();
|
||||
if (file_exists($tempFilePath)) {
|
||||
Log::info('File exists in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||
} else {
|
||||
Log::error('File does not exist in temp directory', ['temp_file_path' => $tempFilePath]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// $diskName = config('media-library.disk_name', 'public');
|
||||
$diskName = config('media-library.disk_name');
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Upload Debug', [
|
||||
'disk' => $diskName,
|
||||
'file_name' => $file->getClientOriginalName(),
|
||||
|
|
@ -97,7 +152,7 @@ class EntryForm
|
|||
$disk->put($testFile, 'test content');
|
||||
$disk->delete($testFile);
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('S3 connectivity test passed');
|
||||
}
|
||||
}
|
||||
|
|
@ -112,12 +167,114 @@ class EntryForm
|
|||
$encodedName = base64_encode($originalName);
|
||||
$secureFileName = Str::random(32) . '-meta' . $encodedName . '-.' . $extension;
|
||||
|
||||
$media = $record->addMedia($file->getRealPath())
|
||||
->usingName($baseName) // Keep original name for display/search
|
||||
->usingFileName($secureFileName) // Use secure filename for storage
|
||||
|
||||
|
||||
|
||||
|
||||
// Resolve possibly-relative Livewire temp path + safe fallbacks
|
||||
$realPath = $realPath ?: (method_exists($file, 'getRealPath') ? $file->getRealPath() : null);
|
||||
$candidates = [];
|
||||
|
||||
if ($realPath && str_starts_with($realPath, '/')) {
|
||||
$candidates[] = $realPath;
|
||||
} else {
|
||||
$candidates[] = sys_get_temp_dir() . '/' . ltrim((string)$realPath, '/');
|
||||
$candidates[] = storage_path('framework/' . ltrim((string)$realPath, '/'));
|
||||
$candidates[] = storage_path(ltrim((string)$realPath, '/'));
|
||||
$candidates[] = base_path(ltrim((string)$realPath, '/'));
|
||||
}
|
||||
|
||||
if ($realPath) {
|
||||
$candidates[] = storage_path('framework/livewire-tmp/' . basename($realPath));
|
||||
$candidates[] = sys_get_temp_dir() . '/' . basename($realPath);
|
||||
}
|
||||
|
||||
// 1) Try storing to local disk (creates an absolute path we control)
|
||||
$stored = null;
|
||||
if (method_exists($file, 'store')) {
|
||||
try {
|
||||
$stored = $file->store('livewire-temp', 'local'); // storage/app/livewire-temp/...
|
||||
if ($stored && \Storage::disk('local')->exists($stored)) {
|
||||
$resolved = \Storage::disk('local')->path($stored);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('store() fallback failed', ['err' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) If not resolved, check candidates
|
||||
if (! isset($resolved)) {
|
||||
foreach ($candidates as $p) {
|
||||
if ($p && file_exists($p)) {
|
||||
$resolved = $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) If still not resolved, try stream -> temp file copy
|
||||
$is_tmp_copy = false;
|
||||
if (! isset($resolved)) {
|
||||
try {
|
||||
$stream = null;
|
||||
if (method_exists($file, 'getStream')) {
|
||||
$stream = $file->getStream();
|
||||
} elseif (method_exists($file, 'getRealPath') && is_readable($file->getRealPath())) {
|
||||
$stream = fopen($file->getRealPath(), 'r');
|
||||
}
|
||||
|
||||
if ($stream) {
|
||||
$tmpPath = tempnam(sys_get_temp_dir(), 'filament-upload-');
|
||||
$out = fopen($tmpPath, 'w');
|
||||
stream_copy_to_stream($stream, $out);
|
||||
fclose($out);
|
||||
if (is_resource($stream)) {
|
||||
@fclose($stream);
|
||||
}
|
||||
$resolved = $tmpPath;
|
||||
$is_tmp_copy = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('stream fallback failed', ['err' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Still nothing -> error
|
||||
if (empty($resolved)) {
|
||||
Log::error('Featured Image Upload: could not resolve temp path', [
|
||||
'original' => $realPath,
|
||||
'checked_candidates' => $candidates,
|
||||
'stored' => $stored,
|
||||
]);
|
||||
throw new \Exception("File `{$realPath}` does not exist");
|
||||
}
|
||||
|
||||
// 5) Use resolved absolute path
|
||||
$media = $record->addMedia($resolved)
|
||||
->usingName($baseName)
|
||||
->usingFileName($secureFileName)
|
||||
->toMediaCollection('featured-image', $diskName);
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
// 6) Cleanup short-lived artifacts
|
||||
if (! empty($is_tmp_copy) && file_exists($resolved)) {
|
||||
@unlink($resolved);
|
||||
}
|
||||
if (! empty($stored) && \Storage::disk('local')->exists($stored)) {
|
||||
\Storage::disk('local')->delete($stored);
|
||||
}
|
||||
|
||||
Log::info('Featured image resolved', ['resolved' => $resolved, 'media_id' => $media->id ?? null]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Upload Success', [
|
||||
'media_id' => $media->id,
|
||||
'media_url' => $media->getUrl(),
|
||||
|
|
@ -189,7 +346,7 @@ class EntryForm
|
|||
$record = $component->getRecord();
|
||||
$diskName = config('media-library.disk_name', 'public');
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Picker Action Debug', [
|
||||
'disk' => $diskName,
|
||||
'record_id' => $record?->id,
|
||||
|
|
@ -223,7 +380,7 @@ class EntryForm
|
|||
try {
|
||||
// For S3, we need to handle file copying differently
|
||||
if ($sourceMedia->disk === 's3') {
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Copying S3 media to new collection', [
|
||||
'source_disk' => $sourceMedia->disk,
|
||||
'source_path' => $sourceMedia->getPathRelativeToRoot(),
|
||||
|
|
@ -267,7 +424,7 @@ class EntryForm
|
|||
->usingFileName($sourceMedia->file_name)
|
||||
->toMediaCollection('featured-image', $diskName);
|
||||
|
||||
if (config('app.env') === 'local') {
|
||||
if (config('logging.channels.' . config('logging.default') . '.level') === 'debug') {
|
||||
Log::info('Featured Image Picker Success', [
|
||||
'new_media_id' => $newMedia->id,
|
||||
'new_media_disk' => $newMedia->disk,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class ListMedia extends ListRecords
|
|||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
// CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use Filament\Forms\Components\Hidden;
|
|||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as SpatieMedia;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MediaForm
|
||||
{
|
||||
|
|
@ -24,6 +26,7 @@ class MediaForm
|
|||
Hidden::make('disk')
|
||||
->default('public'),
|
||||
FileUpload::make('file')
|
||||
->multiple() // workaround for Filament v4 single-file bug
|
||||
->label('File')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
|
|
@ -38,22 +41,50 @@ class MediaForm
|
|||
->acceptedFileTypes(['image/*', 'application/pdf'])
|
||||
->maxSize(10240)
|
||||
->required(fn($context) => $context === 'create')
|
||||
|
||||
|
||||
->afterStateHydrated(function (FileUpload $component, $state, $record): void {
|
||||
if (! $record) {
|
||||
return;
|
||||
Log::info('MediaForm afterStateHydrated invoked', ['record_id' => $record?->id, 'state' => $state]);
|
||||
|
||||
try {
|
||||
if (! $record) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media = $record;
|
||||
if (! $media instanceof SpatieMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct path: {media_id}/{filename}
|
||||
$path = $media->id . '/' . $media->file_name;
|
||||
|
||||
try {
|
||||
$disk = $media->disk ?? 'public';
|
||||
if (Storage::disk($disk)->exists($path)) {
|
||||
$component->state($path);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('MediaForm afterStateHydrated storage check failed', [
|
||||
'err' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'disk' => $media->disk ?? null,
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('MediaForm afterStateHydrated unhandled', [
|
||||
'err' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'record' => $record?->id,
|
||||
'state' => $state,
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$media = $record;
|
||||
|
||||
if (! $media instanceof SpatieMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct the correct path: {media_id}/{filename}
|
||||
$path = $media->id . '/' . $media->file_name;
|
||||
|
||||
$component->state($path);
|
||||
}),
|
||||
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Filament\Resources\Media\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
|
|
@ -19,11 +20,12 @@ class MediaTable
|
|||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->modifyQueryUsing(fn ($query) => $query->where('collection_name', '!=', 'avatars'))
|
||||
->modifyQueryUsing(fn($query) => $query->where('collection_name', '!=', 'avatars'))
|
||||
->columns([
|
||||
ImageColumn::make('url')
|
||||
->label('Preview')
|
||||
->getStateUsing(fn ($record) =>
|
||||
->getStateUsing(
|
||||
fn($record) =>
|
||||
// Prefer the stored path produced by Filament's FileUpload (saved in custom_properties),
|
||||
// fall back to Spatie's getUrl() when no stored_path exists.
|
||||
($record->getCustomProperty('stored_path'))
|
||||
|
|
@ -40,7 +42,7 @@ class MediaTable
|
|||
->badge(),
|
||||
TextColumn::make('mime_type'),
|
||||
TextColumn::make('size')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'),
|
||||
->formatStateUsing(fn($state) => number_format($state / 1024, 2) . ' KB'),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime(),
|
||||
])
|
||||
|
|
@ -52,7 +54,7 @@ class MediaTable
|
|||
]),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
// EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->action(function (Media $record) {
|
||||
// Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path.
|
||||
|
|
@ -67,6 +69,13 @@ class MediaTable
|
|||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
|
||||
Action::make('add')
|
||||
->label('Add')
|
||||
->icon('heroicon-o-plus')
|
||||
->url(fn() => url('admin/assets/create'))
|
||||
->color('primary'),
|
||||
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->action(function (Collection $records) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ use Filament\Tables\Table;
|
|||
|
||||
class TextWidgetResource extends Resource
|
||||
{
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $model = TextWidget::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::DocumentText;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue