feat(helpdesk): polish handover schema editor UI/UX

Auto-generate field keys and option values from labels via slugify,
add live preview panel with hover-highlight, insert-between-fields
dividers, and various spacing/styling improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 18:08:12 +02:00
parent 44153f513f
commit 2ec632ac8b
7 changed files with 494 additions and 221 deletions

View File

@@ -65,7 +65,6 @@ class SoftwareProductService
$fieldNum = $index + 1;
$type = (string) ($field['type'] ?? '');
$label = trim((string) ($field['label'] ?? ''));
$key = trim((string) ($field['key'] ?? ''));
$isDisplayOnly = in_array($type, self::DISPLAY_ONLY_TYPES, true);
if (!in_array($type, self::ALLOWED_FIELD_TYPES, true)) {
@@ -77,13 +76,10 @@ class SoftwareProductService
$errors["field_{$fieldNum}_label"] = t('Field %d: label is required', $fieldNum);
}
if (!$isDisplayOnly) {
if ($key === '') {
$errors["field_{$fieldNum}_key"] = t('Field %d: key is required', $fieldNum);
} elseif (!preg_match('/^[a-z][a-z0-9_]*$/', $key)) {
$errors["field_{$fieldNum}_key"] = t('Field %d: key must start with a letter and contain only lowercase letters, digits, and underscores', $fieldNum);
} elseif (isset($seenKeys[$key])) {
$errors["field_{$fieldNum}_key"] = t('Field %d: duplicate key "%s"', $fieldNum, $key);
if (!$isDisplayOnly && $label !== '') {
$key = self::slugify($label);
if (isset($seenKeys[$key])) {
$errors["field_{$fieldNum}_key"] = t('Field %d: generates duplicate key "%s" — use a more distinct label', $fieldNum, $key);
} else {
$seenKeys[$key] = true;
}
@@ -96,8 +92,8 @@ class SoftwareProductService
} else {
foreach ($options as $optIdx => $opt) {
$optNum = $optIdx + 1;
if (trim((string) ($opt['value'] ?? '')) === '') {
$errors["field_{$fieldNum}_option_{$optNum}"] = t('Field %d, option %d: value is required', $fieldNum, $optNum);
if (trim((string) ($opt['label'] ?? '')) === '') {
$errors["field_{$fieldNum}_option_{$optNum}"] = t('Field %d, option %d: label is required', $fieldNum, $optNum);
}
}
}
@@ -143,16 +139,17 @@ class SoftwareProductService
$entry = ['type' => $type, 'label' => trim((string) ($field['label'] ?? ''))];
if (!$isDisplayOnly) {
$entry['key'] = trim((string) ($field['key'] ?? ''));
$entry['key'] = self::slugify($entry['label']);
$entry['required'] = !empty($field['required']);
}
if ($type === 'select' && is_array($field['options'] ?? null)) {
$entry['options'] = [];
foreach ($field['options'] as $opt) {
$label = trim((string) ($opt['label'] ?? ''));
$entry['options'][] = [
'value' => trim((string) ($opt['value'] ?? '')),
'label' => trim((string) ($opt['label'] ?? '')),
'value' => self::slugify($label),
'label' => $label,
];
}
}
@@ -163,6 +160,19 @@ class SoftwareProductService
return $normalized;
}
private static function slugify(string $text): string
{
$slug = mb_strtolower($text);
$slug = preg_replace('/[äÄ]/', 'ae', $slug);
$slug = preg_replace('/[öÖ]/', 'oe', $slug);
$slug = preg_replace('/[üÜ]/', 'ue', $slug);
$slug = preg_replace('/ß/', 'ss', $slug);
$slug = preg_replace('/[^a-z0-9]+/', '_', $slug);
$slug = trim($slug, '_');
return $slug === '' ? 'option' : $slug;
}
/**
* Update the user-managed name field for a software product.
*