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());
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user