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:
2026-04-16 18:03:49 +02:00
parent d57dcdb616
commit 6a8d21a5b6
31 changed files with 813 additions and 239 deletions

View File

@@ -2,6 +2,7 @@
import { VForm } from 'vuetify/components/VForm'
import { useEmailSettings, useUpdateEmailSettings } from '@/composables/api/useEmail'
import { emailValidator } from '@core/utils/validators'
import ImageUploadField from '@/components/common/ImageUploadField.vue'
import type { AxiosError } from 'axios'
import type { ApiErrorResponse } from '@/types/auth'
@@ -116,34 +117,23 @@ function fieldErrors(field: string): string | undefined {
@submit.prevent="onSubmit"
>
<VRow>
<!-- Logo URL -->
<!-- Logo -->
<VCol
cols="12"
md="8"
>
<VTextField
<ImageUploadField
v-model="logoUrl"
label="Logo URL"
hint="Gebruik een URL naar je logo (PNG of SVG, max 200px hoog)"
persistent-hint
:error-messages="fieldErrors('logo_url')"
label="Logo"
purpose="logo"
hint="PNG, JPG, SVG of WebP. Max 5MB."
:preview-height="80"
/>
<div
v-if="logoUrl"
class="mt-2"
>
<img
:src="logoUrl"
style="max-height: 48px"
@error="($event.target as HTMLImageElement).style.display = 'none'"
@load="($event.target as HTMLImageElement).style.display = 'block'"
>
</div>
<p
v-else
class="text-caption text-disabled mt-2"
v-if="fieldErrors('logo_url')"
class="text-error text-caption mt-1"
>
Geen logo ingesteld
{{ fieldErrors('logo_url') }}
</p>
</VCol>

View File

@@ -68,7 +68,7 @@ const defaultForm = () => ({
label: '',
field_type: 'text' as string,
options: [] as OptionEntry[],
tag_category: null as string | null,
tag_categories: [] as string[],
is_required: false,
is_filterable: false,
is_portal_visible: true,
@@ -113,7 +113,7 @@ function openEditDialog(template: RegistrationFieldTemplate) {
options: template.normalized_options
? template.normalized_options.map(o => ({ label: o.label, description: o.description ?? '' }))
: [],
tag_category: template.tag_category,
tag_categories: template.tag_categories ?? [],
is_required: template.is_required,
is_filterable: template.is_filterable,
is_portal_visible: template.is_portal_visible,
@@ -169,7 +169,7 @@ function onSubmit() {
}
if (showTagCategory.value) {
payload.tag_category = form.value.tag_category || null
payload.tag_categories = form.value.tag_categories.length > 0 ? form.value.tag_categories : null
}
}
@@ -467,81 +467,13 @@ function activate(template: RegistrationFieldTemplate) {
/>
</VCol>
<!-- Options (conditional) -->
<VCol
v-if="showOptions"
cols="12"
>
<label class="text-body-2 mb-2 d-block">Opties</label>
<div
v-for="(option, index) in form.options"
:key="index"
class="mb-3"
>
<VCard
variant="outlined"
class="pa-3 position-relative"
>
<VRow dense>
<VCol cols="12">
<AppTextField
v-model="option.label"
:placeholder="`Optie ${index + 1}`"
density="compact"
hide-details
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="option.description"
label="Beschrijving (optioneel)"
density="compact"
placeholder="Korte toelichting die onder de optie verschijnt"
hint="Max 200 tekens"
counter="200"
maxlength="200"
/>
</VCol>
</VRow>
<VBtn
icon="tabler-x"
variant="text"
color="error"
size="small"
class="position-absolute"
style="inset-block-start: 4px; inset-inline-end: 4px"
@click="removeOption(index)"
/>
</VCard>
</div>
<VBtn
variant="tonal"
size="small"
prepend-icon="tabler-plus"
@click="addOption"
>
Optie toevoegen
</VBtn>
<p
v-if="errors.options"
class="text-error text-caption mt-1"
>
{{ errors.options }}
</p>
</VCol>
<!-- Tag category (conditional) -->
<VCol
v-if="showTagCategory"
cols="12"
>
<AppAutocomplete
v-model="form.tag_category"
label="Tag-categorie"
:items="tagCategories ?? []"
clearable
:error-messages="errors.tag_category"
placeholder="Alle tags (laat leeg voor alle categorieën)"
<VCol cols="12">
<AppTextarea
v-model="form.help_text"
label="Helptekst"
placeholder="Toelichting die onder het veld wordt getoond"
:error-messages="errors.help_text"
rows="2"
/>
</VCol>
@@ -571,15 +503,6 @@ function activate(template: RegistrationFieldTemplate) {
:error-messages="errors.sort_order"
/>
</VCol>
<VCol cols="12">
<AppTextarea
v-model="form.help_text"
label="Helptekst"
placeholder="Toelichting die onder het veld wordt getoond"
:error-messages="errors.help_text"
rows="2"
/>
</VCol>
<!-- Toggles -->
<VCol cols="12">
@@ -610,6 +533,118 @@ function activate(template: RegistrationFieldTemplate) {
/>
</div>
</VCol>
<!-- Options section at bottom with visual separation -->
<template v-if="showOptions">
<VCol cols="12">
<VDivider class="mb-1" />
</VCol>
<VCol cols="12">
<div class="d-flex align-center justify-space-between mb-3">
<div>
<div class="text-subtitle-2">
Opties
</div>
<div class="text-caption text-medium-emphasis">
Waaruit kan de deelnemer kiezen?
</div>
</div>
<VBtn
variant="tonal"
size="small"
prepend-icon="tabler-plus"
@click="addOption"
>
Optie toevoegen
</VBtn>
</div>
<div
v-for="(option, index) in form.options"
:key="index"
class="mb-3"
>
<VCard
variant="outlined"
class="pa-3"
>
<div class="d-flex align-start gap-2">
<VIcon
icon="tabler-grip-vertical"
class="mt-2 text-medium-emphasis"
style="cursor: grab;"
/>
<div class="flex-grow-1">
<AppTextField
v-model="option.label"
:placeholder="`Optie ${index + 1}`"
density="compact"
hide-details="auto"
class="mb-2"
/>
<AppTextField
v-model="option.description"
label="Beschrijving (optioneel)"
density="compact"
placeholder="Korte toelichting die onder de optie verschijnt"
counter="200"
maxlength="200"
hide-details="auto"
/>
</div>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="small"
class="mt-1"
@click="removeOption(index)"
/>
</div>
</VCard>
</div>
<div
v-if="form.options.length === 0"
class="text-center text-medium-emphasis py-4"
>
Nog geen opties. Klik op "Optie toevoegen" om te beginnen.
</div>
<p
v-if="errors.options"
class="text-error text-caption mt-1"
>
{{ errors.options }}
</p>
</VCol>
</template>
<!-- Tag categories (conditional) -->
<template v-if="showTagCategory">
<VCol cols="12">
<VDivider class="mb-1" />
</VCol>
<VCol cols="12">
<div class="text-subtitle-2 mb-1">
Tag-categorieën
</div>
<div class="text-caption text-medium-emphasis mb-3">
Kies één of meerdere categorieën. De deelnemer ziet alle tags uit deze categorieën.
</div>
<AppAutocomplete
v-model="form.tag_categories"
label="Categorieën"
:items="tagCategories ?? []"
multiple
chips
closable-chips
clearable
:error-messages="errors.tag_categories"
placeholder="Alle tags (laat leeg voor alle categorieën)"
/>
</VCol>
</template>
</template>
</VRow>
</VCardText>