Skip to content

Commit 1b236e8

Browse files
authored
Merge branch 'main' into update-select-style-to-match-input
2 parents 6a0bf1d + 31d5721 commit 1b236e8

File tree

9 files changed

+254
-27
lines changed

9 files changed

+254
-27
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Http\Controllers\Admin;
6+
7+
use App\Http\Controllers\Controller;
8+
use Illuminate\Http\JsonResponse;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Http\UploadedFile;
11+
use Illuminate\Support\Facades\Storage;
12+
use Illuminate\Support\Str;
13+
use Intervention\Image\Facades\Image;
14+
15+
class UploadController extends Controller
16+
{
17+
public function __invoke(Request $request): JsonResponse
18+
{
19+
$request->validate([
20+
'upload' => [
21+
'required',
22+
'file',
23+
'image',
24+
'mimes:jpeg,png,jpg,gif',
25+
],
26+
]);
27+
28+
if ($request->hasFile('upload')) {
29+
30+
$file = $request->file('upload');
31+
32+
if (is_array($file)) {
33+
$file = $file[0];
34+
}
35+
36+
if (! $file instanceof UploadedFile) {
37+
return response()->json(['error' => 'Invalid upload'], 400);
38+
}
39+
40+
$originalName = $file->getClientOriginalName();
41+
$extension = $file->getClientOriginalExtension();
42+
43+
$name = Str::slug(date('Y-m-d-h-i-s').'-'.pathinfo($originalName, PATHINFO_FILENAME));
44+
$image = Image::make($file);
45+
46+
$imageString = $image->stream()->__toString();
47+
$name = "$name.$extension";
48+
49+
Storage::disk('images')
50+
->put('uploads/'.$name, $imageString);
51+
52+
return response()->json([
53+
'url' => "/images/uploads/$name",
54+
]);
55+
}
56+
57+
return response()->json(['error' => 'No file uploaded'], 422);
58+
}
59+
}

app/Http/Middleware/VerifyCsrfToken.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ class VerifyCsrfToken extends Middleware
1414
* @var array<int, string>
1515
*/
1616
protected $except = [
17-
//
17+
'admin/image-upload',
1818
];
1919
}

config/filesystems.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
return [
4+
5+
/*
6+
|--------------------------------------------------------------------------
7+
| Default Filesystem Disk
8+
|--------------------------------------------------------------------------
9+
|
10+
| Here you may specify the default filesystem disk that should be used
11+
| by the framework. The "local" disk, as well as a variety of cloud
12+
| based disks are available to your application. Just store away!
13+
|
14+
*/
15+
16+
'default' => env('FILESYSTEM_DRIVER', 'local'),
17+
18+
/*
19+
|--------------------------------------------------------------------------
20+
| Filesystem Disks
21+
|--------------------------------------------------------------------------
22+
|
23+
| Here you may configure as many filesystem "disks" as you wish, and you
24+
| may even configure multiple disks of the same driver. Defaults have
25+
| been setup for each driver as an example of the required options.
26+
|
27+
| Supported Drivers: "local", "ftp", "sftp", "s3"
28+
|
29+
*/
30+
31+
'disks' => [
32+
33+
'local' => [
34+
'driver' => 'local',
35+
'root' => storage_path('app'),
36+
],
37+
38+
'public' => [
39+
'driver' => 'local',
40+
'root' => storage_path('app/public'),
41+
'url' => env('APP_URL').'/storage',
42+
'visibility' => 'public',
43+
],
44+
45+
'images' => [
46+
'driver' => 'local',
47+
'root' => public_path('images'),
48+
'url' => env('APP_URL').'/images',
49+
'visibility' => 'public',
50+
],
51+
52+
's3' => [
53+
'driver' => 's3',
54+
'key' => env('AWS_ACCESS_KEY_ID'),
55+
'secret' => env('AWS_SECRET_ACCESS_KEY'),
56+
'region' => env('AWS_DEFAULT_REGION'),
57+
'bucket' => env('AWS_BUCKET'),
58+
'url' => env('AWS_URL'),
59+
'endpoint' => env('AWS_ENDPOINT'),
60+
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
61+
],
62+
63+
],
64+
65+
/*
66+
|--------------------------------------------------------------------------
67+
| Symbolic Links
68+
|--------------------------------------------------------------------------
69+
|
70+
| Here you may configure the symbolic links that will be created when the
71+
| `storage:link` Artisan command is executed. The array keys should be
72+
| the locations of the links and the values should be their targets.
73+
|
74+
*/
75+
76+
'links' => [
77+
public_path('storage') => storage_path('app/public'),
78+
],
79+
80+
];

public/js/ckeditor5.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/views/components/form/ckeditor.blade.php

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@push('scripts')
2-
<script src="/js/ckeditor/ckeditor.js"></script>
2+
<script src="{{ url('js/ckeditor5.js') }}"></script>
33
@endpush
44

55
@props([
@@ -22,24 +22,31 @@
2222
@endif
2323
<div wire:ignore class="mt-5">
2424
@if ($label !='none')
25-
<x-form.label :$label :$required :$name />
25+
<label for="{{ $name }}" class="block text-sm font-medium leading-5 text-gray-700 dark:text-gray-200">{{ $label }} @if ($required != '') <span aria-hidden="true" class="error">*</span>@endif</label>
2626
@endif
2727
<textarea
28-
id="{{ $name }}"
2928
x-data
3029
x-init="
31-
editor = CKEDITOR.replace($refs.item);
32-
editor.on('change', function(event){
33-
@this.set('{{ $name }}', event.editor.getData());
34-
})
30+
ClassicEditor
31+
.create($refs.item, {
32+
simpleUpload: {
33+
uploadUrl: '{{ url('admin/image-upload') }}'
34+
}
35+
})
36+
.then(editor => {
37+
editor.model.document.on('change:data', () => {
38+
@this.set('{{ $name }}', editor.getData());
39+
})
40+
})
41+
.catch(error => {
42+
console.error(error);
43+
});
3544
"
3645
x-ref="item"
3746
{{ $attributes }}
38-
@error($name)
39-
aria-invalid="true"
40-
aria-description="{{ $message }}"
41-
@enderror
42-
>{{ $slot }}</textarea>
47+
>
48+
{{ $slot }}
49+
</textarea>
4350
</div>
4451
@error($name)
4552
<p class="error" aria-live="assertive">{{ $message }}</p>

resources/views/components/layouts/app.blade.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<meta name="csrf-token" content="{{ csrf_token() }}">
77
<title>@yield('title') {{ $title ?? null }} - {{ config('app.name', 'Laravel') }}</title>
8+
@stack('scripts')
89
@vite(['resources/css/app.css', 'resources/js/app.js'])
910
</head>
1011
<body>

resources/views/components/layouts/app/navigation.blade.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,6 @@
33
<x-nav.link route="dashboard" icon="home">{{ __('Dashboard') }}</x-nav.link>
44
@endcan
55

6-
{{--<x-nav.group label="{{__('Settings')}}" route="admin.settings" icon="cog">--}}
7-
{{-- @can('view_audit_trails')--}}
8-
{{-- <x-nav.group-item route="admin.settings.audit-trails.index" icon="identification">{{__('Audit Trails')}}</x-nav.group-item>--}}
9-
{{-- @endcan--}}
10-
11-
{{-- @can('view_roles')--}}
12-
{{-- <x-nav.group-item route="admin.settings.roles.index" icon="archive-box">{{__('Roles')}}</x-nav.group-item>--}}
13-
{{-- @endcan--}}
14-
15-
{{-- @can('view_system_settings')--}}
16-
{{-- <x-nav.group-item route="admin.settings" icon="wrench-screwdriver">{{__('System Settings')}}</x-nav.group-item>--}}
17-
{{-- @endcan--}}
18-
{{--</x-nav.group>--}}
19-
206
@if(can('view_system_settings') || can('view_roles') || can('view_audit_trails'))
217
<x-nav.divider>{{ __('Settings') }}</x-nav.divider>
228
@endif

routes/web.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Controllers\Admin\UploadController;
34
use App\Http\Controllers\Auth\TwoFaController;
45
use App\Http\Controllers\WelcomeController;
56
use App\Livewire\Admin\AuditTrails;
@@ -22,6 +23,8 @@
2223
Route::prefix(config('admintw.prefix'))->middleware(['auth', 'verified', 'activeUser', 'ipCheckMiddleware'])->group(function () {
2324
Route::get('/', Dashboard::class)->name('dashboard');
2425

26+
Route::post('image-upload', UploadController::class)->name('image-upload');
27+
2528
Route::view('developer-reference', 'developer-reference')
2629
->name('developer-reference');
2730

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
use App\Http\Controllers\Admin\UploadController;
4+
use Illuminate\Http\Request;
5+
use Illuminate\Http\UploadedFile;
6+
use Illuminate\Support\Facades\Storage;
7+
8+
use function Pest\Laravel\postJson;
9+
10+
beforeEach(function () {
11+
Storage::fake('images');
12+
$this->user = $this->authenticate();
13+
});
14+
15+
test('uploads an image successfully', function () {
16+
$file = UploadedFile::fake()->image('example.jpg');
17+
18+
$response = postJson(route('image-upload'), [
19+
'upload' => $file,
20+
])
21+
->assertStatus(200)
22+
->assertJsonStructure(['url']);
23+
24+
$path = str_replace('/images/', '', $response->json('url'));
25+
Storage::disk('images')->assertExists($path);
26+
});
27+
28+
test('handles valid upload when file is sent as an array', function () {
29+
$file = UploadedFile::fake()->image('photo.jpg');
30+
31+
$mockRequest = Mockery::mock(Request::class);
32+
$mockRequest->shouldReceive('validate')->andReturn(['upload' => [$file]]);
33+
$mockRequest->shouldReceive('hasFile')->once()->with('upload')->andReturn(true);
34+
$mockRequest->shouldReceive('file')->once()->with('upload')->andReturn([$file]);
35+
36+
$controller = new UploadController;
37+
38+
$response = $controller($mockRequest);
39+
40+
expect($response->getStatusCode())->toBe(200);
41+
expect($response->getData(true))->toHaveKey('url');
42+
43+
$path = str_replace('/images/', '', $response->getData(true)['url']);
44+
Storage::disk('images')->assertExists($path);
45+
});
46+
47+
test('fails when no file is uploaded', function () {
48+
postJson(route('image-upload'), [])->assertStatus(422);
49+
});
50+
51+
test('fails when file is not an image', function () {
52+
$file = UploadedFile::fake()->create('example.txt', 10, 'text/plain');
53+
54+
postJson(route('image-upload'), [
55+
'upload' => $file,
56+
])->assertStatus(422);
57+
});
58+
59+
test('returns 400 when uploaded file is not an instance of UploadedFile', function () {
60+
$mockRequest = Mockery::mock(Request::class);
61+
62+
$mockRequest->shouldReceive('validate')->andReturn(['upload' => 'not-a-file']);
63+
$mockRequest->shouldReceive('hasFile')->once()->with('upload')->andReturn(true);
64+
$mockRequest->shouldReceive('file')->once()->with('upload')->andReturn('not-a-file-object');
65+
66+
$controller = new UploadController;
67+
68+
$response = $controller($mockRequest);
69+
70+
expect($response->getStatusCode())->toBe(400);
71+
expect($response->getData(true)['error'])->toBe('Invalid upload');
72+
});
73+
74+
test('returns 422 when upload is present but hasFile returns false', function () {
75+
$mockRequest = Mockery::mock(Request::class);
76+
$mockRequest->shouldReceive('validate')->andReturn(['upload' => 'not-a-real-file']);
77+
$mockRequest->shouldReceive('hasFile')->once()->with('upload')->andReturn(false);
78+
79+
$controller = new UploadController;
80+
81+
$response = $controller($mockRequest);
82+
83+
expect($response->getStatusCode())->toBe(422);
84+
expect($response->getData(true)['error'])->toBe('No file uploaded');
85+
});

0 commit comments

Comments
 (0)