feat: registration field polish, multi-category tags, file uploads, Partner icon
- Restructure field editor dialog: move Options section to bottom with divider and subheader, fix delete button with flex layout - Change tag_category (single string) to tag_categories (JSON array) supporting multiple category selection in tag picker fields - Portal tag picker now groups tags by category with subheaders - Add generic file upload endpoint (FileUploadService + UploadController) - Replace email branding logo URL text field with ImageUploadField - Update Partner crowd type default icon to tabler-affiliate - Apply changes consistently to both field and template dialogs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ final class PublicRegistrationDataController extends Controller
|
||||
'field_type' => $field->field_type->value,
|
||||
'options' => $field->options,
|
||||
'normalized_options' => $field->normalized_options,
|
||||
'tag_category' => $field->tag_category,
|
||||
'tag_categories' => $field->tag_categories,
|
||||
'is_required' => $field->is_required,
|
||||
'help_text' => $field->help_text,
|
||||
'display_width' => $field->display_width->value,
|
||||
@@ -108,11 +108,11 @@ final class PublicRegistrationDataController extends Controller
|
||||
$query = PersonTag::where('organisation_id', $organisationId)
|
||||
->where('is_active', true);
|
||||
|
||||
if ($field->tag_category) {
|
||||
$query->where('category', $field->tag_category);
|
||||
if (!empty($field->tag_categories)) {
|
||||
$query->whereIn('category', $field->tag_categories);
|
||||
}
|
||||
|
||||
$data['available_tags'] = $query->orderBy('sort_order')
|
||||
$data['available_tags'] = $query->orderBy('category')->orderBy('sort_order')
|
||||
->get()
|
||||
->map(fn (PersonTag $tag) => [
|
||||
'id' => $tag->id,
|
||||
|
||||
52
api/app/Http/Controllers/Api/V1/UploadController.php
Normal file
52
api/app/Http/Controllers/Api/V1/UploadController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\FileUploadService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class UploadController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FileUploadService $uploadService,
|
||||
) {}
|
||||
|
||||
public function uploadImage(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => ['required', 'file', 'max:5120'],
|
||||
'purpose' => ['required', 'string', 'in:logo,banner,icon,avatar'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$organisation = $user->organisations()->first();
|
||||
|
||||
try {
|
||||
$url = $this->uploadService->uploadImage(
|
||||
file: $request->file('file'),
|
||||
directory: 'uploads/' . $request->input('purpose'),
|
||||
organisationId: $organisation?->id,
|
||||
);
|
||||
} catch (\DomainException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
activity('upload')
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'purpose' => $request->input('purpose'),
|
||||
'original_name' => $request->file('file')->getClientOriginalName(),
|
||||
'size_bytes' => $request->file('file')->getSize(),
|
||||
'mime' => $request->file('file')->getMimeType(),
|
||||
])
|
||||
->log('image.uploaded');
|
||||
|
||||
return response()->json([
|
||||
'data' => ['url' => $url],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,11 @@ final class StoreRegistrationFieldTemplateRequest extends FormRequest
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => [
|
||||
'tag_categories' => [
|
||||
$type === RegistrationFieldType::TAG_PICKER ? 'nullable' : 'prohibited',
|
||||
'string',
|
||||
'max:50',
|
||||
'array',
|
||||
],
|
||||
'tag_categories.*' => ['string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
|
||||
@@ -43,11 +43,11 @@ final class StoreRegistrationFormFieldRequest extends FormRequest
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => [
|
||||
'tag_categories' => [
|
||||
$type === RegistrationFieldType::TAG_PICKER ? 'nullable' : 'prohibited',
|
||||
'string',
|
||||
'max:50',
|
||||
'array',
|
||||
],
|
||||
'tag_categories.*' => ['string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
|
||||
@@ -25,7 +25,8 @@ final class UpdateRegistrationFieldTemplateRequest extends FormRequest
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => ['nullable', 'string', 'max:50'],
|
||||
'tag_categories' => ['nullable', 'array'],
|
||||
'tag_categories.*' => ['string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_filterable' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
|
||||
@@ -24,7 +24,8 @@ final class UpdateRegistrationFormFieldRequest extends FormRequest
|
||||
'options.*' => ['required'],
|
||||
'options.*.label' => ['required_if:options.*,array', 'string', 'max:255'],
|
||||
'options.*.description' => ['nullable', 'string', 'max:200'],
|
||||
'tag_category' => ['nullable', 'string', 'max:50'],
|
||||
'tag_categories' => ['nullable', 'array'],
|
||||
'tag_categories.*' => ['string', 'max:50'],
|
||||
'is_required' => ['nullable', 'boolean'],
|
||||
'is_portal_visible' => ['nullable', 'boolean'],
|
||||
'is_admin_only' => ['nullable', 'boolean'],
|
||||
|
||||
@@ -19,7 +19,7 @@ final class RegistrationFieldTemplateResource extends JsonResource
|
||||
'field_type' => $this->field_type->value,
|
||||
'options' => $this->options,
|
||||
'normalized_options' => $this->normalized_options,
|
||||
'tag_category' => $this->tag_category,
|
||||
'tag_categories' => $this->tag_categories,
|
||||
'is_required' => $this->is_required,
|
||||
'is_filterable' => $this->is_filterable,
|
||||
'is_portal_visible' => $this->is_portal_visible,
|
||||
|
||||
@@ -21,7 +21,7 @@ final class RegistrationFormFieldResource extends JsonResource
|
||||
'field_type' => $this->field_type->value,
|
||||
'options' => $this->options,
|
||||
'normalized_options' => $this->normalized_options,
|
||||
'tag_category' => $this->tag_category,
|
||||
'tag_categories' => $this->tag_categories,
|
||||
'is_required' => $this->is_required,
|
||||
'is_portal_visible' => $this->is_portal_visible,
|
||||
'is_admin_only' => $this->is_admin_only,
|
||||
@@ -37,11 +37,11 @@ final class RegistrationFormFieldResource extends JsonResource
|
||||
$query = PersonTag::where('organisation_id', $this->event->organisation_id)
|
||||
->where('is_active', true);
|
||||
|
||||
if ($this->tag_category) {
|
||||
$query->where('category', $this->tag_category);
|
||||
if (!empty($this->tag_categories)) {
|
||||
$query->whereIn('category', $this->tag_categories);
|
||||
}
|
||||
|
||||
return PersonTagResource::collection($query->orderBy('sort_order')->get());
|
||||
return PersonTagResource::collection($query->orderBy('category')->orderBy('sort_order')->get());
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
@@ -29,7 +29,7 @@ final class RegistrationFieldTemplate extends Model
|
||||
'slug',
|
||||
'field_type',
|
||||
'options',
|
||||
'tag_category',
|
||||
'tag_categories',
|
||||
'is_required',
|
||||
'is_filterable',
|
||||
'is_portal_visible',
|
||||
@@ -46,6 +46,7 @@ final class RegistrationFieldTemplate extends Model
|
||||
return [
|
||||
'field_type' => RegistrationFieldType::class,
|
||||
'options' => 'array',
|
||||
'tag_categories' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'is_filterable' => 'boolean',
|
||||
'is_portal_visible' => 'boolean',
|
||||
|
||||
@@ -33,7 +33,7 @@ final class RegistrationFormField extends Model
|
||||
'slug',
|
||||
'field_type',
|
||||
'options',
|
||||
'tag_category',
|
||||
'tag_categories',
|
||||
'is_required',
|
||||
'is_portal_visible',
|
||||
'is_admin_only',
|
||||
@@ -48,6 +48,7 @@ final class RegistrationFormField extends Model
|
||||
return [
|
||||
'field_type' => RegistrationFieldType::class,
|
||||
'options' => 'array',
|
||||
'tag_categories' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'is_portal_visible' => 'boolean',
|
||||
'is_admin_only' => 'boolean',
|
||||
|
||||
67
api/app/Services/FileUploadService.php
Normal file
67
api/app/Services/FileUploadService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class FileUploadService
|
||||
{
|
||||
private const ALLOWED_IMAGE_MIMES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
'image/gif',
|
||||
];
|
||||
|
||||
private const MAX_IMAGE_SIZE_KB = 5120; // 5MB
|
||||
|
||||
public function uploadImage(
|
||||
UploadedFile $file,
|
||||
string $directory,
|
||||
?string $organisationId = null,
|
||||
): string {
|
||||
$this->validateImage($file);
|
||||
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$filename = Str::ulid() . '.' . $extension;
|
||||
$path = trim($directory, '/');
|
||||
|
||||
if ($organisationId) {
|
||||
$path .= '/' . $organisationId;
|
||||
}
|
||||
|
||||
Storage::disk('public')->putFileAs($path, $file, $filename);
|
||||
|
||||
return Storage::disk('public')->url($path . '/' . $filename);
|
||||
}
|
||||
|
||||
public function deleteByUrl(string $url): void
|
||||
{
|
||||
$baseUrl = Storage::disk('public')->url('');
|
||||
$path = str_replace($baseUrl, '', $url);
|
||||
|
||||
if ($path && Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateImage(UploadedFile $file): void
|
||||
{
|
||||
if (!in_array($file->getMimeType(), self::ALLOWED_IMAGE_MIMES, true)) {
|
||||
throw new \DomainException(
|
||||
'Alleen JPG, PNG, WebP, SVG of GIF bestanden zijn toegestaan.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($file->getSize() / 1024 > self::MAX_IMAGE_SIZE_KB) {
|
||||
throw new \DomainException(
|
||||
'Bestand is te groot. Maximum: 5MB.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ final class RegistrationFieldTemplateService
|
||||
'slug' => $slug,
|
||||
'field_type' => $template->field_type,
|
||||
'options' => $template->options,
|
||||
'tag_category' => $template->tag_category,
|
||||
'tag_categories' => $template->tag_categories,
|
||||
'is_required' => $template->is_required,
|
||||
'is_portal_visible' => $template->is_portal_visible,
|
||||
'is_admin_only' => $template->is_admin_only,
|
||||
@@ -155,7 +155,7 @@ final class RegistrationFieldTemplateService
|
||||
['label' => 'EHBO / BHV diploma', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 10],
|
||||
['label' => 'Rijbewijs', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 11],
|
||||
['label' => 'Eerder vrijwilliger geweest', 'field_type' => 'boolean', 'is_filterable' => true, 'display_width' => 'half', 'sort_order' => 12],
|
||||
['label' => 'Certificaten & vaardigheden', 'field_type' => 'tag_picker', 'tag_category' => null, 'is_filterable' => true, 'display_width' => 'full', 'sort_order' => 13],
|
||||
['label' => 'Certificaten & vaardigheden', 'field_type' => 'tag_picker', 'tag_categories' => null, 'is_filterable' => true, 'display_width' => 'full', 'sort_order' => 13],
|
||||
['label' => 'Aanvullende informatie', 'field_type' => 'heading', 'display_width' => 'full', 'sort_order' => 14],
|
||||
['label' => 'Toestemming gegevensverwerking', 'field_type' => 'boolean', 'is_required' => true, 'help_text' => 'Ik geef toestemming voor de verwerking van mijn persoonsgegevens ten behoeve van de organisatie van dit evenement, conform de Algemene Verordening Gegevensbescherming (AVG).', 'display_width' => 'full', 'sort_order' => 15],
|
||||
['label' => 'Opmerkingen', 'field_type' => 'textarea', 'display_width' => 'full', 'sort_order' => 16],
|
||||
|
||||
@@ -171,7 +171,7 @@ final class RegistrationFormFieldService
|
||||
'slug' => $slug,
|
||||
'field_type' => $sourceField->field_type,
|
||||
'options' => $sourceField->options,
|
||||
'tag_category' => $sourceField->tag_category,
|
||||
'tag_categories' => $sourceField->tag_categories,
|
||||
'is_required' => $sourceField->is_required,
|
||||
'is_portal_visible' => $sourceField->is_portal_visible,
|
||||
'is_admin_only' => $sourceField->is_admin_only,
|
||||
|
||||
@@ -27,7 +27,7 @@ final class RegistrationFieldTemplateFactory extends Factory
|
||||
'slug' => Str::slug($label),
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'options' => null,
|
||||
'tag_category' => null,
|
||||
'tag_categories' => null,
|
||||
'is_required' => false,
|
||||
'is_filterable' => false,
|
||||
'is_portal_visible' => true,
|
||||
|
||||
@@ -27,7 +27,7 @@ final class RegistrationFormFieldFactory extends Factory
|
||||
'slug' => Str::slug($label),
|
||||
'field_type' => RegistrationFieldType::TEXT,
|
||||
'options' => null,
|
||||
'tag_category' => null,
|
||||
'tag_categories' => null,
|
||||
'is_required' => false,
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// registration_form_fields: add tag_categories JSON, migrate data, drop tag_category
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->json('tag_categories')->nullable()->after('options');
|
||||
});
|
||||
|
||||
DB::table('registration_form_fields')
|
||||
->whereNotNull('tag_category')
|
||||
->where('tag_category', '!=', '')
|
||||
->eachById(function ($row) {
|
||||
DB::table('registration_form_fields')
|
||||
->where('id', $row->id)
|
||||
->update(['tag_categories' => json_encode([$row->tag_category])]);
|
||||
});
|
||||
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->dropColumn('tag_category');
|
||||
});
|
||||
|
||||
// registration_field_templates: add tag_categories JSON, migrate data, drop tag_category
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->json('tag_categories')->nullable()->after('options');
|
||||
});
|
||||
|
||||
DB::table('registration_field_templates')
|
||||
->whereNotNull('tag_category')
|
||||
->where('tag_category', '!=', '')
|
||||
->eachById(function ($row) {
|
||||
DB::table('registration_field_templates')
|
||||
->where('id', $row->id)
|
||||
->update(['tag_categories' => json_encode([$row->tag_category])]);
|
||||
});
|
||||
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->dropColumn('tag_category');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->string('tag_category', 50)->nullable()->after('options');
|
||||
});
|
||||
|
||||
DB::table('registration_form_fields')
|
||||
->whereNotNull('tag_categories')
|
||||
->eachById(function ($row) {
|
||||
$categories = json_decode($row->tag_categories, true);
|
||||
DB::table('registration_form_fields')
|
||||
->where('id', $row->id)
|
||||
->update(['tag_category' => $categories[0] ?? null]);
|
||||
});
|
||||
|
||||
Schema::table('registration_form_fields', function (Blueprint $table) {
|
||||
$table->dropColumn('tag_categories');
|
||||
});
|
||||
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->string('tag_category', 50)->nullable()->after('options');
|
||||
});
|
||||
|
||||
DB::table('registration_field_templates')
|
||||
->whereNotNull('tag_categories')
|
||||
->eachById(function ($row) {
|
||||
$categories = json_decode($row->tag_categories, true);
|
||||
DB::table('registration_field_templates')
|
||||
->where('id', $row->id)
|
||||
->update(['tag_category' => $categories[0] ?? null]);
|
||||
});
|
||||
|
||||
Schema::table('registration_field_templates', function (Blueprint $table) {
|
||||
$table->dropColumn('tag_categories');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('crowd_types')
|
||||
->where('system_type', 'PARTNER')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('icon')
|
||||
->orWhere('icon', '')
|
||||
->orWhere('icon', 'tabler-handshake');
|
||||
})
|
||||
->update(['icon' => 'tabler-affiliate']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('crowd_types')
|
||||
->where('system_type', 'PARTNER')
|
||||
->where('icon', 'tabler-affiliate')
|
||||
->update(['icon' => 'tabler-handshake']);
|
||||
}
|
||||
};
|
||||
@@ -124,7 +124,7 @@ class DevSeeder extends Seeder
|
||||
['name' => 'Pers', 'system_type' => 'PRESS', 'color' => '#FF9800', 'icon' => 'tabler-camera'],
|
||||
['name' => 'Gast', 'system_type' => 'GUEST', 'color' => '#607D8B', 'icon' => 'tabler-ticket'],
|
||||
['name' => 'Leverancier', 'system_type' => 'SUPPLIER', 'color' => '#795548', 'icon' => 'tabler-truck-delivery'],
|
||||
['name' => 'Partner', 'system_type' => 'PARTNER', 'color' => '#FFC107', 'icon' => 'tabler-handshake'],
|
||||
['name' => 'Partner', 'system_type' => 'PARTNER', 'color' => '#FFC107', 'icon' => 'tabler-affiliate'],
|
||||
];
|
||||
|
||||
foreach ($crowdTypesData as $data) {
|
||||
|
||||
@@ -38,6 +38,7 @@ use App\Http\Controllers\Api\V1\EmailChangeController;
|
||||
use App\Http\Controllers\Api\V1\PasswordResetController;
|
||||
use App\Http\Controllers\Api\V1\PortalMeController;
|
||||
use App\Http\Controllers\Api\V1\Portal\PortalShiftController;
|
||||
use App\Http\Controllers\Api\V1\UploadController;
|
||||
use App\Http\Controllers\Api\V1\UserOrganisationTagController;
|
||||
use App\Http\Controllers\Api\V1\Admin\AdminOrganisationController;
|
||||
use App\Http\Controllers\Api\V1\Admin\AdminUserController;
|
||||
@@ -146,6 +147,9 @@ Route::middleware(['auth:sanctum', 'impersonation'])->group(function () {
|
||||
Route::delete('auth/trusted-devices/{device}', [TrustedDeviceController::class, 'destroy']);
|
||||
Route::delete('auth/trusted-devices', [TrustedDeviceController::class, 'destroyAll']);
|
||||
|
||||
// File uploads
|
||||
Route::post('upload/image', [UploadController::class, 'uploadImage']);
|
||||
|
||||
// Account management (self-service)
|
||||
Route::put('me/profile', [AccountController::class, 'updateProfile']);
|
||||
Route::post('me/change-password', [AccountController::class, 'changePassword']);
|
||||
|
||||
@@ -317,7 +317,7 @@ class PublicRegistrationDataTest extends TestCase
|
||||
|
||||
RegistrationFormField::factory()->tagPickerField()->create([
|
||||
'event_id' => $event->id,
|
||||
'tag_category' => 'Certificaat',
|
||||
'tag_categories' => ['Certificaat'],
|
||||
'is_portal_visible' => true,
|
||||
'is_admin_only' => false,
|
||||
]);
|
||||
|
||||
125
api/tests/Feature/Api/V1/UploadTest.php
Normal file
125
api/tests/Feature/Api/V1/UploadTest.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\V1;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UploadTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $user;
|
||||
private Organisation $organisation;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
|
||||
$this->organisation = Organisation::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->organisation->users()->attach($this->user, ['role' => 'org_admin']);
|
||||
|
||||
Storage::fake('public');
|
||||
}
|
||||
|
||||
public function test_upload_image_succeeds_with_valid_png(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$file = UploadedFile::fake()->image('logo.png', 200, 200);
|
||||
|
||||
$response = $this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'logo',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonStructure(['data' => ['url']]);
|
||||
|
||||
$url = $response->json('data.url');
|
||||
$this->assertStringContainsString('uploads/logo', $url);
|
||||
}
|
||||
|
||||
public function test_upload_image_rejects_unsupported_mime(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100, 'application/pdf');
|
||||
|
||||
$response = $this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'logo',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_upload_image_rejects_oversized_file(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$file = UploadedFile::fake()->image('large.png')->size(6000); // 6MB > 5MB limit
|
||||
|
||||
$response = $this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'logo',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_upload_image_requires_authentication(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->image('logo.png', 200, 200);
|
||||
|
||||
$response = $this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'logo',
|
||||
]);
|
||||
|
||||
$response->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_upload_image_logs_activity(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$file = UploadedFile::fake()->image('logo.png', 200, 200);
|
||||
|
||||
$this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'logo',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'log_name' => 'upload',
|
||||
'description' => 'image.uploaded',
|
||||
'causer_id' => $this->user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_upload_image_requires_valid_purpose(): void
|
||||
{
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$file = UploadedFile::fake()->image('logo.png', 200, 200);
|
||||
|
||||
$response = $this->postJson('/api/v1/upload/image', [
|
||||
'file' => $file,
|
||||
'purpose' => 'invalid',
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors('purpose');
|
||||
}
|
||||
}
|
||||
@@ -121,32 +121,32 @@ class RegistrationFormFieldTest extends TestCase
|
||||
->assertJsonValidationErrors('options');
|
||||
}
|
||||
|
||||
public function test_store_tag_picker_accepts_tag_category(): void
|
||||
public function test_store_tag_picker_accepts_tag_categories(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Vaardigheden',
|
||||
'field_type' => 'tag_picker',
|
||||
'tag_category' => 'Vaardigheid',
|
||||
'tag_categories' => ['Vaardigheid', 'Horeca'],
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.tag_category', 'Vaardigheid');
|
||||
->assertJsonPath('data.tag_categories', ['Vaardigheid', 'Horeca']);
|
||||
}
|
||||
|
||||
public function test_store_text_field_rejects_tag_category(): void
|
||||
public function test_store_text_field_rejects_tag_categories(): void
|
||||
{
|
||||
Sanctum::actingAs($this->orgAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/registration-fields", [
|
||||
'label' => 'Naam',
|
||||
'field_type' => 'text',
|
||||
'tag_category' => 'Vaardigheid',
|
||||
'tag_categories' => ['Vaardigheid'],
|
||||
]);
|
||||
|
||||
$response->assertUnprocessable()
|
||||
->assertJsonValidationErrors('tag_category');
|
||||
->assertJsonValidationErrors('tag_categories');
|
||||
}
|
||||
|
||||
public function test_slug_uniqueness_per_event(): void
|
||||
|
||||
Reference in New Issue
Block a user