diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 53a8641a..accd89b5 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -865,6 +865,8 @@ "externalFolder": "Externer Ordner", "externalFolderHint": "Diese Ordner stammen aus dem /external-media Mount innerhalb des Containers oder Hosts.", "externalFolderRequired": "Bitte wählen Sie vor dem Speichern einen externen Ordner aus.", + "externalFolderEmpty": "Keine Unterordner", + "clearSelection": "Löschen", "welcomeMessage": "Willkommensnachricht", "noWelcomeMessage": "Keine Willkommensnachricht festgelegt", "noWelcomeMessageSet": "Keine Willkommensnachricht festgelegt", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 59da8cfb..20efb40b 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -416,6 +416,8 @@ "externalFolder": "External Folder", "externalFolderHint": "These folders are read from the /external-media mount inside your container or host.", "externalFolderRequired": "Please select an external folder before saving.", + "externalFolderEmpty": "No subfolders", + "clearSelection": "Clear", "welcomeMessage": "Welcome Message", "noWelcomeMessage": "No welcome message set", "created": "Created", diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index fbbaa4f6..f1f00717 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -410,6 +410,8 @@ "externalFolder": "Externe map", "externalFolderHint": "Deze mappen worden gelezen vanuit de /external-media mount in uw container of host.", "externalFolderRequired": "Selecteer een externe map voordat u opslaat.", + "externalFolderEmpty": "Geen submappen", + "clearSelection": "Wissen", "welcomeMessage": "Welkomstbericht", "noWelcomeMessage": "Geen welkomstbericht ingesteld", "created": "Aangemaakt", diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index 1e5c8b73..1d32bb90 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -410,6 +410,8 @@ "externalFolder": "Pasta Externa", "externalFolderHint": "Essas pastas são lidas do ponto de montagem /external-media dentro do seu container ou host.", "externalFolderRequired": "Selecione uma pasta externa antes de salvar.", + "externalFolderEmpty": "Sem subpastas", + "clearSelection": "Limpar", "welcomeMessage": "Mensagem de Boas-Vindas", "noWelcomeMessage": "Nenhuma mensagem de boas-vindas definida", "created": "Criado", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index e895c762..bb038675 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -410,6 +410,8 @@ "externalFolder": "Внешняя папка", "externalFolderHint": "Эти папки считываются из монтирования /external-media внутри контейнера или на хосте.", "externalFolderRequired": "Выберите внешнюю папку перед сохранением.", + "externalFolderEmpty": "Нет вложенных папок", + "clearSelection": "Очистить", "welcomeMessage": "Приветственное сообщение", "noWelcomeMessage": "Приветственное сообщение не задано", "created": "Создано", diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 244dc2f2..f06ff64c 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -28,7 +28,12 @@ import { MousePointer, Layout, Trash2, - Send + Send, + ChevronRight, + ChevronDown, + Folder, + FolderOpen, + Loader2 } from 'lucide-react'; import { parseISO, differenceInDays, isValid } from 'date-fns'; @@ -67,60 +72,166 @@ import { feedbackService, FeedbackSettings as FeedbackSettingsType } from '../.. import { cssTemplatesService, type EnabledTemplate } from '../../services/cssTemplates.service'; import { ThemeConfig, GALLERY_THEME_PRESETS } from '../../types/theme.types'; +const FolderTreeNode: React.FC<{ + path: string; + name: string; + depth: number; + value: string; + onChange: (p: string) => void; + expandedPaths: Set; + toggleExpand: (p: string) => void; +}> = ({ path, name, depth, value, onChange, expandedPaths, toggleExpand }) => { + const { t } = useTranslation(); + const isExpanded = expandedPaths.has(path); + const isSelected = value === path; + + const { data, isLoading, isError } = useQuery({ + queryKey: ['external-folder-children', path], + queryFn: () => externalMediaService.list(path), + enabled: isExpanded, + staleTime: 30_000 + }); + + const dirs = (data?.entries || []).filter((e: any) => e.type === 'dir'); + const showEmpty = isExpanded && !isLoading && !isError && dirs.length === 0; + const indentStyle = { paddingLeft: depth * 16 + 4 }; + const childIndentStyle = { paddingLeft: (depth + 1) * 16 + 4 }; + const rowClass = + 'flex items-center gap-1 py-1 pr-1 rounded ' + + (isSelected + ? 'bg-primary-50 dark:bg-primary-900/30' + : 'hover:bg-neutral-50 dark:hover:bg-neutral-700'); + + return ( +
+
+ + +
+ {isExpanded && ( +
+ {isLoading && ( +
+ + {t('common.loading', 'Loading...')} +
+ )} + {isError && ( +
+ {t('errors.somethingWentWrong', 'Something went wrong')} +
+ )} + {showEmpty && ( +
+ {t('events.externalFolderEmpty', 'No subfolders')} +
+ )} + {dirs.map((d: any) => { + const childPath = path ? `${path}/${d.name}` : d.name; + return ( + + ); + })} +
+ )} +
+ ); +}; + const ExternalFolderPicker: React.FC<{ value: string; onChange: (p: string) => void }> = ({ value, onChange }) => { const { t } = useTranslation(); - const [entries, setEntries] = useState<{ path: string; entries: any[]; canNavigateUp: boolean } | null>(null); - const [loading, setLoading] = useState(false); - const [currentPath, setCurrentPath] = useState(value || ''); - const load = async (p: string) => { - try { - setLoading(true); - const res = await externalMediaService.list(p); - setEntries(res); - setCurrentPath(res.path); - } finally { - setLoading(false); + // Seed expanded paths so the current selection (and the synthetic root) is visible on mount. + const [expandedPaths, setExpandedPaths] = useState>(() => { + const set = new Set(['']); + if (value) { + const parts = value.split('/').filter(Boolean); + let acc = ''; + for (const seg of parts) { + acc = acc ? `${acc}/${seg}` : seg; + set.add(acc); + } } - }; + return set; + }); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => { load(currentPath || ''); }, []); - - const navigateUp = () => { - if (!entries?.canNavigateUp) return; - const parts = (entries.path || '').split('/').filter(Boolean); - parts.pop(); - load(parts.join('/')); + const toggleExpand = (p: string) => { + setExpandedPaths(prev => { + const next = new Set(prev); + if (next.has(p)) next.delete(p); + else next.add(p); + return next; + }); }; return ( -
-
-
/external-media/{entries?.path || ''}
-
- - +
+
+
+ {t('common.selected', 'Selected')}: /external-media/{value}
+ {value && ( + + )} +
+
+
- {loading ? ( -
{t('common.loading', 'Loading...')}
- ) : ( -
- {entries?.entries?.filter((e: any) => e.type === 'dir').map((e: any) => ( - - ))} -
- )} - {value && ( -
{t('common.selected', 'Selected')}: /external-media/{value}
- )}
); };