fix(p06): P-06-Fragen direkt in Exercise-Upload-Modals eingebettet
Some checks failed
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Failing after 1m41s

Kein zweites Dialog-Overlay mehr. Die Rechte-Erklarung ist jetzt
direkt als Formular-Abschnitt im Upload-Tab von
ExerciseInlineFileMediaModal und ExerciseInlineEmbedModal integriert.

- Datei auswaehlen + Titel + Rechte-Erklaerung in einem Schritt
- Validierung beim Klick auf Hochladen (Fehler inline angezeigt)
- RightsDeclarationDialog-Import entfernt
- radio-Namen eindeutig per Praefix (up-/emb-) damit beide Modals
  unabhaengig voneinander funktionieren

version: 0.8.76

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-11 08:44:37 +02:00
parent 000e78e976
commit 9ac8200b41
2 changed files with 345 additions and 41 deletions

View File

@ -9,7 +9,40 @@ import {
sanitizeInlineMediaSize,
} from '../constants/inlineExerciseMedia'
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
import RightsDeclarationDialog from './RightsDeclarationDialog'
const DECL_INIT = {
rights_holder_confirmed: false,
contains_identifiable_persons: null,
person_consent_confirmed: false,
contains_minors: null,
parental_consent_confirmed: false,
contains_music: null,
music_rights_confirmed: false,
contains_third_party_content: null,
third_party_rights_confirmed: false,
}
function validateDecl(decl) {
if (!decl.rights_holder_confirmed)
return 'Bitte bestätigen, dass du die erforderlichen Rechte an diesem Medium besitzt.'
if (decl.contains_identifiable_persons === null)
return 'Bitte angeben, ob erkennbare Personen abgebildet sind.'
if (decl.contains_identifiable_persons && !decl.person_consent_confirmed)
return 'Bitte bestätigen, dass die Einwilligungen aller erkennbaren Personen vorliegen.'
if (decl.contains_minors === null)
return 'Bitte angeben, ob Minderjährige abgebildet sind.'
if (decl.contains_minors && !decl.parental_consent_confirmed)
return 'Bitte bestätigen, dass die Einwilligungen der Sorgeberechtigten vorliegen.'
if (decl.contains_music === null)
return 'Bitte angeben, ob das Medium Musik enthält.'
if (decl.contains_music && !decl.music_rights_confirmed)
return 'Bitte bestätigen, dass die erforderlichen Musikrechte vorliegen.'
if (decl.contains_third_party_content === null)
return 'Bitte angeben, ob fremde geschützte Inhalte enthalten sind.'
if (decl.contains_third_party_content && !decl.third_party_rights_confirmed)
return 'Bitte bestätigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen.'
return ''
}
/**
* @param {{
@ -31,27 +64,32 @@ export default function ExerciseInlineEmbedModal({
const [title, setTitle] = useState('')
const [displaySize, setDisplaySize] = useState(DEFAULT_INLINE_MEDIA_SIZE)
const [busy, setBusy] = useState(false)
const [rightsDialogOpen, setRightsDialogOpen] = useState(false)
const [decl, setDecl] = useState({ ...DECL_INIT })
const [declErr, setDeclErr] = useState('')
const setDeclField = (key, val) => setDecl((d) => ({ ...d, [key]: val }))
useEffect(() => {
if (!open) return
setUrl('')
setTitle('')
setDisplaySize(DEFAULT_INLINE_MEDIA_SIZE)
setDecl({ ...DECL_INIT })
setDeclErr('')
}, [open])
const submit = () => {
const submit = async () => {
const u = url.trim()
if (!u) {
alert('Bitte eine Embed-URL eingeben (https://…).')
return
}
setRightsDialogOpen(true)
}
const doSubmitWithDecl = async (decl) => {
setRightsDialogOpen(false)
const u = url.trim()
const validErr = validateDecl(decl)
if (validErr) {
setDeclErr(validErr)
return
}
setDeclErr('')
const size = sanitizeInlineMediaSize(displaySize)
const fd = new FormData()
fd.append('embed_url', u)
@ -86,21 +124,13 @@ export default function ExerciseInlineEmbedModal({
if (!open) return null
return (
<>
<RightsDeclarationDialog
open={rightsDialogOpen}
onCancel={() => setRightsDialogOpen(false)}
onConfirm={doSubmitWithDecl}
targetVisibility="private"
mode="upload"
/>
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && !rightsDialogOpen && onClose()}>
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && onClose()}>
<div
className="admin-modal-sheet"
role="dialog"
aria-modal="true"
aria-labelledby="rte-inline-embed-title"
style={{ maxWidth: '480px', width: '100%' }}
style={{ maxWidth: '480px', width: '100%', maxHeight: '90vh', display: 'flex', flexDirection: 'column' }}
onClick={(e) => e.stopPropagation()}
>
<div className="admin-modal-sheet__header">
@ -111,7 +141,7 @@ export default function ExerciseInlineEmbedModal({
Schließen
</button>
</div>
<div style={{ padding: '14px 16px' }}>
<div style={{ overflowY: 'auto', flex: 1, padding: '14px 16px' }}>
<label className="form-label">Embed-URL</label>
<input
type="url"
@ -141,12 +171,135 @@ export default function ExerciseInlineEmbedModal({
</option>
))}
</select>
<button type="button" className="btn btn-primary btn-full" style={{ marginTop: '16px' }} disabled={busy} onClick={submit}>
{/* P-06 Rechte-Erklärung */}
<div style={{ marginTop: '18px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}>
<p style={{ fontSize: '0.82rem', fontWeight: 600, marginBottom: '10px', color: 'var(--text2)' }}>
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG p06-v1-conservative)</span>
</p>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
<input
type="checkbox"
id="emb-rhc"
checked={decl.rights_holder_confirmed}
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
disabled={busy}
style={{ marginTop: '3px', flexShrink: 0 }}
/>
<label htmlFor="emb-rhc" style={{ fontSize: '0.85rem' }}>
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
</label>
</div>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === true}
onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cip" checked={decl.contains_identifiable_persons === false}
onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_identifiable_persons === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="emb-pcc" checked={decl.person_consent_confirmed}
onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="emb-pcc" style={{ fontSize: '0.85rem' }}>
Einwilligungen aller erkennbaren Personen liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cm" checked={decl.contains_minors === true}
onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cm" checked={decl.contains_minors === false}
onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_minors === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="emb-pcc2" checked={decl.parental_consent_confirmed}
onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="emb-pcc2" style={{ fontSize: '0.85rem' }}>
Einwilligungen der Sorgeberechtigten liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cmu" checked={decl.contains_music === true}
onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-cmu" checked={decl.contains_music === false}
onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_music === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="emb-mrc" checked={decl.music_rights_confirmed}
onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="emb-mrc" style={{ fontSize: '0.85rem' }}>
Musikrechte (GEMA / Lizenz) liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === true}
onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="emb-ctpc" checked={decl.contains_third_party_content === false}
onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_third_party_content === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="emb-tprc" checked={decl.third_party_rights_confirmed}
onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="emb-tprc" style={{ fontSize: '0.85rem' }}>
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
</label>
</div>
)}
</fieldset>
{declErr && (
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '8px' }}>{declErr}</p>
)}
</div>
</div>
<div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
<button type="button" className="btn btn-primary btn-full" disabled={busy} onClick={submit}>
{busy ? 'Speichern…' : 'Hinzufügen & in Text einfügen'}
</button>
</div>
</div>
</div>
</>
)
}

View File

@ -10,7 +10,6 @@ import {
sanitizeInlineMediaSize,
} from '../constants/inlineExerciseMedia'
import { sanitizeInlineMediaCaption } from '../utils/inlineMediaCaption'
import RightsDeclarationDialog from './RightsDeclarationDialog'
function RtePickerAssetThumb({ asset }) {
const id = asset.id
@ -61,6 +60,40 @@ function inferExerciseMediaType(file) {
return 'image'
}
const DECL_INIT = {
rights_holder_confirmed: false,
contains_identifiable_persons: null,
person_consent_confirmed: false,
contains_minors: null,
parental_consent_confirmed: false,
contains_music: null,
music_rights_confirmed: false,
contains_third_party_content: null,
third_party_rights_confirmed: false,
}
function validateDecl(decl) {
if (!decl.rights_holder_confirmed)
return 'Bitte bestätigen, dass du die erforderlichen Rechte an diesem Medium besitzt.'
if (decl.contains_identifiable_persons === null)
return 'Bitte angeben, ob erkennbare Personen abgebildet sind.'
if (decl.contains_identifiable_persons && !decl.person_consent_confirmed)
return 'Bitte bestätigen, dass die Einwilligungen aller erkennbaren Personen vorliegen.'
if (decl.contains_minors === null)
return 'Bitte angeben, ob Minderjährige abgebildet sind.'
if (decl.contains_minors && !decl.parental_consent_confirmed)
return 'Bitte bestätigen, dass die Einwilligungen der Sorgeberechtigten vorliegen.'
if (decl.contains_music === null)
return 'Bitte angeben, ob das Medium Musik enthält.'
if (decl.contains_music && !decl.music_rights_confirmed)
return 'Bitte bestätigen, dass die erforderlichen Musikrechte vorliegen.'
if (decl.contains_third_party_content === null)
return 'Bitte angeben, ob fremde geschützte Inhalte enthalten sind.'
if (decl.contains_third_party_content && !decl.third_party_rights_confirmed)
return 'Bitte bestätigen, dass die Rechte an allen enthaltenen Fremdmaterialien vorliegen.'
return ''
}
/**
* @param {{
* open: boolean,
@ -90,7 +123,10 @@ export default function ExerciseInlineFileMediaModal({
const [uploadTitle, setUploadTitle] = useState('')
const [displaySize, setDisplaySize] = useState(DEFAULT_INLINE_MEDIA_SIZE)
const [uploadInputKey, setUploadInputKey] = useState(0)
const [rightsDialogOpen, setRightsDialogOpen] = useState(false)
const [decl, setDecl] = useState({ ...DECL_INIT })
const [declErr, setDeclErr] = useState('')
const setDeclField = (key, val) => setDecl((d) => ({ ...d, [key]: val }))
const assetToExerciseMedia = useMemo(() => {
const m = new Map()
@ -128,6 +164,8 @@ export default function ExerciseInlineFileMediaModal({
setUploadInputKey((k) => k + 1)
setDisplaySize(DEFAULT_INLINE_MEDIA_SIZE)
setErr(null)
setDecl({ ...DECL_INIT })
setDeclErr('')
const t = setTimeout(loadAssets, 280)
return () => clearTimeout(t)
}, [open])
@ -184,16 +222,17 @@ export default function ExerciseInlineFileMediaModal({
}
}
const handleUploadAndInsert = () => {
const handleUploadAndInsert = async () => {
if (!uploadFile) {
alert('Bitte eine Datei wählen.')
return
}
setRightsDialogOpen(true)
}
const doUploadWithDecl = async (decl) => {
setRightsDialogOpen(false)
const validErr = validateDecl(decl)
if (validErr) {
setDeclErr(validErr)
return
}
setDeclErr('')
const size = sanitizeInlineMediaSize(displaySize)
const inferred = inferExerciseMediaType(uploadFile)
const fd = new FormData()
@ -242,15 +281,7 @@ export default function ExerciseInlineFileMediaModal({
if (!open) return null
return (
<>
<RightsDeclarationDialog
open={rightsDialogOpen}
onCancel={() => setRightsDialogOpen(false)}
onConfirm={doUploadWithDecl}
targetVisibility="private"
mode="upload"
/>
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && !rightsDialogOpen && onClose()}>
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && !busy && onClose()}>
<div
className="admin-modal-sheet rte-inline-media-modal"
role="dialog"
@ -339,7 +370,7 @@ export default function ExerciseInlineFileMediaModal({
})}
</div>
{!loading && items.length === 0 ? (
<p style={{ color: 'var(--text3)', marginTop: '12px' }}>Keine Treffer Suche anpassen oder Neu hochladen.</p>
<p style={{ color: 'var(--text3)', marginTop: '12px' }}>Keine Treffer Suche anpassen oder Neu hochladen".</p>
) : null}
</>
)}
@ -376,6 +407,127 @@ export default function ExerciseInlineFileMediaModal({
onChange={(e) => setUploadTitle(e.target.value)}
disabled={busy}
/>
{/* P-06 Rechte-Erklärung */}
<div style={{ marginTop: '18px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}>
<p style={{ fontSize: '0.82rem', fontWeight: 600, marginBottom: '10px', color: 'var(--text2)' }}>
Rechte-Erklärung <span style={{ fontWeight: 400, color: 'var(--text3)' }}>(VORLÄUFIG p06-v1-conservative)</span>
</p>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginBottom: '10px' }}>
<input
type="checkbox"
id="up-rhc"
checked={decl.rights_holder_confirmed}
onChange={(e) => setDeclField('rights_holder_confirmed', e.target.checked)}
disabled={busy}
style={{ marginTop: '3px', flexShrink: 0 }}
/>
<label htmlFor="up-rhc" style={{ fontSize: '0.85rem' }}>
Ich bestätige, dass ich die erforderlichen Rechte an diesem Medium besitze oder zur Veröffentlichung berechtigt bin. *
</label>
</div>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Erkennbare Personen abgebildet? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === true}
onChange={() => setDeclField('contains_identifiable_persons', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cip" checked={decl.contains_identifiable_persons === false}
onChange={() => setDeclField('contains_identifiable_persons', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_identifiable_persons === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="up-pcc" checked={decl.person_consent_confirmed}
onChange={(e) => setDeclField('person_consent_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="up-pcc" style={{ fontSize: '0.85rem' }}>
Einwilligungen aller erkennbaren Personen liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Minderjährige abgebildet? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cm" checked={decl.contains_minors === true}
onChange={() => setDeclField('contains_minors', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cm" checked={decl.contains_minors === false}
onChange={() => setDeclField('contains_minors', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_minors === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="up-pcc2" checked={decl.parental_consent_confirmed}
onChange={(e) => setDeclField('parental_consent_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="up-pcc2" style={{ fontSize: '0.85rem' }}>
Einwilligungen der Sorgeberechtigten liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '10px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Musik enthalten? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cmu" checked={decl.contains_music === true}
onChange={() => setDeclField('contains_music', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-cmu" checked={decl.contains_music === false}
onChange={() => setDeclField('contains_music', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_music === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="up-mrc" checked={decl.music_rights_confirmed}
onChange={(e) => setDeclField('music_rights_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="up-mrc" style={{ fontSize: '0.85rem' }}>
Musikrechte (GEMA / Lizenz) liegen vor. *
</label>
</div>
)}
</fieldset>
<fieldset style={{ border: 'none', padding: 0, marginBottom: '4px' }}>
<legend style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: '4px' }}>Fremde geschützte Inhalte (Logos, Grafiken, Fotos Dritter)? *</legend>
<div style={{ display: 'flex', gap: '14px' }}>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === true}
onChange={() => setDeclField('contains_third_party_content', true)} disabled={busy} /> Ja
</label>
<label style={{ fontSize: '0.85rem' }}>
<input type="radio" name="up-ctpc" checked={decl.contains_third_party_content === false}
onChange={() => setDeclField('contains_third_party_content', false)} disabled={busy} /> Nein
</label>
</div>
{decl.contains_third_party_content === true && (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', marginTop: '6px' }}>
<input type="checkbox" id="up-tprc" checked={decl.third_party_rights_confirmed}
onChange={(e) => setDeclField('third_party_rights_confirmed', e.target.checked)}
disabled={busy} style={{ marginTop: '3px', flexShrink: 0 }} />
<label htmlFor="up-tprc" style={{ fontSize: '0.85rem' }}>
Rechte an allen enthaltenen Fremdmaterialien liegen vor. *
</label>
</div>
)}
</fieldset>
{declErr && (
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '8px' }}>{declErr}</p>
)}
</div>
</>
)}
</div>
@ -410,6 +562,5 @@ export default function ExerciseInlineFileMediaModal({
</div>
</div>
</div>
</>
)
}