Files
infoscreen/dashboard/src/components/CustomSelectUploadEventModal.tsx
Olaf 2580aa5e0d docs: extract frontend design rules and add presentation persistence fix
Create FRONTEND_DESIGN_RULES.md as the single source of truth for all dashboard
UI conventions, including component library (Syncfusion first), component defaults
table, layout structure, buttons, dialogs, badges, toasts, form fields, tabs,
statistics cards, warnings, color palette, CSS files, loading states, locale
rules, and icon conventions (TentTree for skip-holidays events).

Move embedded design rules from ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md to the new
file and replace with a reference link for maintainability.

Update copilot-instructions.md to point to FRONTEND_DESIGN_RULES.md and remove
redundant Syncfusion/Tailwind prose from the Theming section.

Add reference blockquote to README.md under Frontend Features directing readers
to FRONTEND_DESIGN_RULES.md.

Bug fix: Presentation events now reliably persist page_progress and auto_progress
flags across create, update, and detached occurrence flows so display settings
survive round-trips to the API.

Files changed:
- Created: FRONTEND_DESIGN_RULES.md (15 sections, 340+ lines)
- Modified: ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md (extract rules, consolidate)
- Modified: .github/copilot-instructions.md (link to new rules file)
- Modified: README.md (reference blockquote)
2026-03-31 07:29:42 +00:00

151 lines
4.6 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { useAuth } from '../useAuth';
import { DialogComponent } from '@syncfusion/ej2-react-popups';
import {
FileManagerComponent,
Inject,
NavigationPane,
DetailsView,
Toolbar,
} from '@syncfusion/ej2-react-filemanager';
const hostUrl = '/api/eventmedia/filemanager/';
type CustomSelectUploadEventModalProps = {
open: boolean;
onClose: () => void;
onSelect: (file: { id: string; path: string; name: string }) => void; // name ergänzt
selectedFileId?: string | null;
};
const CustomSelectUploadEventModal: React.FC<CustomSelectUploadEventModalProps> = props => {
const { open, onClose, onSelect } = props;
const { user } = useAuth();
const isSuperadmin = useMemo(() => user?.role === 'superadmin', [user]);
const [selectedFile, setSelectedFile] = useState<{
id: string;
path: string;
name: string;
} | null>(null);
const [selectionError, setSelectionError] = useState<string>('');
// Callback für Dateiauswahl
interface FileSelectEventArgs {
fileDetails: {
name: string;
isFile: boolean;
size: number;
// weitere Felder falls benötigt
};
}
const handleFileSelect = async (args: FileSelectEventArgs) => {
if (args.fileDetails.isFile && args.fileDetails.size > 0) {
const filename = args.fileDetails.name;
setSelectionError('');
try {
const response = await fetch(
`/api/eventmedia/find_by_filename?filename=${encodeURIComponent(filename)}`
);
if (response.ok) {
const data = await response.json();
setSelectedFile({ id: data.id, path: data.file_path, name: filename });
} else {
setSelectedFile(null);
setSelectionError('Datei ist noch nicht als Medium registriert. Bitte erneut hochladen oder Metadaten prüfen.');
}
} catch (e) {
console.error('Error fetching file details:', e);
setSelectedFile(null);
setSelectionError('Medium-ID konnte nicht geladen werden. Bitte erneut versuchen.');
}
}
};
// Button-Handler
const handleSelectClick = () => {
if (selectedFile) {
onSelect(selectedFile);
}
};
type FileItem = { name: string; isFile: boolean };
type ReadSuccessArgs = { action: string; result?: { files?: FileItem[] } };
type FileOpenArgs = { fileDetails?: FileItem; cancel?: boolean };
const handleSuccess = (args: ReadSuccessArgs) => {
if (isSuperadmin) return;
if (args && args.action === 'read' && args.result && Array.isArray(args.result.files)) {
args.result.files = args.result.files.filter((f: FileItem) => !(f.name === 'converted' && !f.isFile));
}
};
const handleFileOpen = (args: FileOpenArgs) => {
if (!isSuperadmin && args && args.fileDetails && args.fileDetails.name === 'converted' && !args.fileDetails.isFile) {
args.cancel = true;
}
};
return (
<DialogComponent
target="#root"
visible={open}
width="700px"
header="Medium auswählen/hochladen"
showCloseIcon={true}
close={onClose}
isModal={true}
footerTemplate={() => (
<div className="flex gap-2 justify-end">
<button className="e-btn" onClick={onClose}>
Abbrechen
</button>
<button className="e-btn e-primary" disabled={!selectedFile} onClick={handleSelectClick}>
Auswählen
</button>
</div>
)}
>
<FileManagerComponent
cssClass="e-bigger media-icons-xl"
success={handleSuccess}
fileOpen={handleFileOpen}
ajaxSettings={{
url: hostUrl + 'operations',
getImageUrl: hostUrl + 'get-image',
uploadUrl: hostUrl + 'upload',
downloadUrl: hostUrl + 'download',
}}
toolbarSettings={{
items: [
'NewFolder',
'Upload',
'Download',
'Rename',
'Delete',
'SortBy',
'Refresh',
'Details',
],
}}
contextMenuSettings={{
file: ['Open', '|', 'Download', '|', 'Rename', 'Delete', '|', 'Details'],
folder: ['Open', '|', 'Rename', 'Delete', '|', 'Details'],
layout: ['SortBy', 'Refresh', '|', 'View', 'Details'],
}}
allowMultiSelection={false}
fileSelect={handleFileSelect}
>
<Inject services={[NavigationPane, DetailsView, Toolbar]} />
</FileManagerComponent>
{selectionError && (
<div style={{ marginTop: 10, color: '#b71c1c', fontSize: 13 }}>{selectionError}</div>
)}
</DialogComponent>
);
};
export default CustomSelectUploadEventModal;