diff --git a/CLAUDE.md b/CLAUDE.md index c782956..34fdf1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,6 +166,7 @@ docker/ # Dockerfiles, Nginx configs, PHP configs - New views must handle loading, empty, error, and success states (GR-UI-014) - All visible text wrapped in `t('...')` - Check existing JS/CSS components before adding new ones (GR-UI-REUSE) +- **File uploads** use `app-file-upload.phtml` partial (deliberate exception to plain-HTML inputs — the markup is too complex for inline use). See `templates/partials/app-file-upload.phtml` for the `$fileUpload` array contract. Never write file-upload markup inline; always use the partial. ### Never Do This diff --git a/config/assets.php b/config/assets.php index 6d6b865..c598da6 100644 --- a/config/assets.php +++ b/config/assets.php @@ -17,6 +17,7 @@ return [ 'css/components/app-forms.css', 'css/components/app-flash.css', 'css/components/app-brand.css', + 'css/components/app-file-upload.css', ], 'default' => [ 'css/vendor-overrides/multi-select.css', diff --git a/docs/reference-entwickler-checkliste.md b/docs/reference-entwickler-checkliste.md index 6bfefa2..3260164 100644 --- a/docs/reference-entwickler-checkliste.md +++ b/docs/reference-entwickler-checkliste.md @@ -21,6 +21,7 @@ Kurze Definition of Done vor Merge. - [ ] Listen-Filter konsistent: IDs/Enums als Select/MultiSelect statt Freitext - [ ] Für persistierte Status/Outcomes nur zentrale Taxonomien aus `lib/Domain/Taxonomy/*` nutzen (keine lokalen String-Literale) - [ ] Für neue/angepasste Liste: `filter-schema.php` angelegt/aktualisiert +- [ ] File-Uploads über `templates/partials/app-file-upload.phtml` (kein inline File-Upload-Markup) — `$fileUpload`-Array mit `name`, `accept`, `hint`, optional `currentSrc`/`deleteAction` - [ ] Listen-Titlebar über `templates/partials/app-list-titlebar.phtml` (kein inline `
`) - [ ] Listen-Tabs über `templates/partials/app-list-tabs.phtml` (kein inline `
`) - [ ] Purge-Titlebar-Action über `templates/partials/app-list-purge-action.phtml` (kein inline Purge-Form/Button) diff --git a/i18n/default_de.json b/i18n/default_de.json index c39174c..339a2b6 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -1430,5 +1430,10 @@ "Username or password not valid": "Benutzername oder Passwort ungültig", "Write-only. Leave empty to keep current value.": "Nur Schreibzugriff. Leer lassen, um den aktuellen Wert beizubehalten.", "Your account is deactivated.": "Ihr Konto ist deaktiviert.", - "LDAP": "LDAP" + "LDAP": "LDAP", + "Drop file here or click to select": "Datei hierher ziehen oder klicken", + "Remove selected file": "Ausgewaehlte Datei entfernen", + "Replace": "Ersetzen", + "Delete this image?": "Dieses Bild löschen?", + "Current image": "Aktuelles Bild" } diff --git a/i18n/default_en.json b/i18n/default_en.json index bfe620f..7dc456a 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -1430,5 +1430,10 @@ "Username or password not valid": "Username or password not valid", "Write-only. Leave empty to keep current value.": "Write-only. Leave empty to keep current value.", "Your account is deactivated.": "Your account is deactivated.", - "LDAP": "LDAP" + "LDAP": "LDAP", + "Drop file here or click to select": "Drop file here or click to select", + "Remove selected file": "Remove selected file", + "Replace": "Replace", + "Delete this image?": "Delete this image?", + "Current image": "Current image" } diff --git a/pages/admin/imports/index(default).phtml b/pages/admin/imports/index(default).phtml index fae6e16..ac328bd 100644 --- a/pages/admin/imports/index(default).phtml +++ b/pages/admin/imports/index(default).phtml @@ -87,13 +87,16 @@ $renderErrorTable = static function (array $rows): void { -
- -
+ + 'csv_file', + 'accept' => '.csv,.txt,text/csv', + 'hint' => t('Max size 10MB, max 20000 rows'), + 'required' => true, + ]; + require templatePath('partials/app-file-upload.phtml'); + ?>
diff --git a/pages/admin/settings/index(default).phtml b/pages/admin/settings/index(default).phtml index 51e85d4..0dc1462 100644 --- a/pages/admin/settings/index(default).phtml +++ b/pages/admin/settings/index(default).phtml @@ -770,24 +770,20 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
- - - -
- -
- - - - -
+ 'logo', + 'accept' => 'image/svg+xml,image/png,image/jpeg,image/webp', + 'hint' => t('Allowed file types: SVG, PNG, JPG, WEBP'), + 'currentSrc' => $hasLogo ? appLogoUrl(128) : '', + 'deleteAction' => $hasLogo ? 'admin/settings/logo-delete' : '', + ]; + require templatePath('partials/app-file-upload.phtml'); + ?> +
@@ -799,31 +795,23 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
- -
- <?php e(t('Favicon')); ?> -
- -
- -
- -
- - - - -
+ 'favicon', + 'accept' => 'image/png', + 'hint' => t('Allowed file types: PNG'), + 'currentSrc' => $hasFavicon ? asset('favicon/favicon-32x32.png') : '', + 'currentLabel' => t('Favicon'), + 'deleteAction' => $hasFavicon ? 'admin/settings/favicon-delete' : '', + ]; + require templatePath('partials/app-file-upload.phtml'); + ?> +
diff --git a/pages/admin/tenants/edit(default).phtml b/pages/admin/tenants/edit(default).phtml index 9c75aa2..cc473ec 100644 --- a/pages/admin/tenants/edit(default).phtml +++ b/pages/admin/tenants/edit(default).phtml @@ -104,20 +104,19 @@ $hasFavicon = $avatarUuid !== '' && $tenantFaviconService->hasFavicon($avatarUui
- -
- - - - -
+ 'avatar', + 'accept' => 'image/*', + 'hint' => t('Allowed file types: SVG, PNG, JPG, WEBP'), + 'currentSrc' => $hasAvatar ? 'admin/tenants/avatar-file?uuid=' . $avatarUuid . '&size=128' : '', + 'deleteAction' => $hasAvatar ? 'admin/tenants/avatar-delete/' . $avatarUuid : '', + ]; + require templatePath('partials/app-file-upload.phtml'); + ?> +
@@ -129,33 +128,24 @@ $hasFavicon = $avatarUuid !== '' && $tenantFaviconService->hasFavicon($avatarUui
- -
- <?php e(t('Favicon')); ?> -
- -
- -
- -
- - - - -
+ 'favicon', + 'accept' => 'image/png', + 'hint' => t('Allowed file types: PNG'), + 'currentSrc' => $hasFavicon ? asset('favicon/tenants/' . $avatarUuid . '/favicon/favicon-32x32.png') : '', + 'currentLabel' => t('Favicon'), + 'deleteAction' => $hasFavicon ? 'admin/tenants/favicon-delete/' . $avatarUuid : '', + ]; + require templatePath('partials/app-file-upload.phtml'); + ?> +
diff --git a/pages/admin/users/edit(default).phtml b/pages/admin/users/edit(default).phtml index 5d3f7e0..c124cfb 100644 --- a/pages/admin/users/edit(default).phtml +++ b/pages/admin/users/edit(default).phtml @@ -252,20 +252,19 @@ if ($lastLoginAt !== '') {
- -
- - - - -
+ 'avatar', + 'accept' => 'image/*', + 'hint' => t('Allowed file types: SVG, PNG, JPG, WEBP'), + 'currentSrc' => $hasAvatar ? 'admin/users/avatar-file?uuid=' . $avatarUuid . '&size=128' : '', + 'deleteAction' => $hasAvatar ? 'admin/users/avatar-delete/' . $avatarUuid : '', + ]; + require templatePath('partials/app-file-upload.phtml'); + ?> +
diff --git a/templates/partials/app-file-upload.phtml b/templates/partials/app-file-upload.phtml new file mode 100644 index 0000000..e223999 --- /dev/null +++ b/templates/partials/app-file-upload.phtml @@ -0,0 +1,79 @@ + 'avatar', // required — input name attribute + * 'accept' => 'image/*', // required — accepted file types + * 'hint' => t('Allowed file types: SVG, PNG, JPG, WEBP'),// optional — hint text below dropzone label + * 'currentSrc' => 'admin/tenants/avatar-file?uuid=…&size=128', // optional — URL of existing server file + * 'currentLabel' => t('Current image'), // optional — label for current file preview + * 'deleteAction' => 'admin/tenants/avatar-delete/…', // optional — formaction for delete button (requires currentSrc) + * 'deleteConfirm' => t('Delete this image?'), // optional — confirm message on delete + * 'required' => false, // optional — make input required + * ]; + * require templatePath('partials/app-file-upload.phtml'); + * + * @var array $fileUpload + */ + +$fileUpload = is_array($fileUpload ?? null) ? $fileUpload : []; +$uploadName = trim((string) ($fileUpload['name'] ?? '')); +if ($uploadName === '') { + return; +} + +$uploadAccept = trim((string) ($fileUpload['accept'] ?? '')); +$uploadHint = trim((string) ($fileUpload['hint'] ?? '')); +$uploadCurrentSrc = trim((string) ($fileUpload['currentSrc'] ?? '')); +$uploadCurrentLabel = trim((string) ($fileUpload['currentLabel'] ?? t('Current image'))); +$uploadDeleteAction = trim((string) ($fileUpload['deleteAction'] ?? '')); +$uploadDeleteConfirm = trim((string) ($fileUpload['deleteConfirm'] ?? t('Delete this image?'))); +$uploadRequired = (bool) ($fileUpload['required'] ?? false); +$hasCurrent = $uploadCurrentSrc !== ''; +$hasDelete = $hasCurrent && $uploadDeleteAction !== ''; + +?> +
data-current-src="" data-current-label=""> + +
+ +
+ + + + + + + +
+
+ + +
+ + + + + + + +
+
diff --git a/templates/partials/app-main-aside.phtml b/templates/partials/app-main-aside.phtml index 3affa99..7ba69b7 100644 --- a/templates/partials/app-main-aside.phtml +++ b/templates/partials/app-main-aside.phtml @@ -285,7 +285,7 @@ $modulePanelSlots = is_array($moduleSlots['aside.tab_panel'] ?? null) ? $moduleS