feat(P-13): Club-Admin Bearbeitung + Archiv-Trennung in Inbox
Some checks failed
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Failing after 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 1m3s
Some checks failed
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Failing after 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 10s
Test Suite / playwright-tests (push) Successful in 1m3s
Berechtigungen: - Club-Admins koennen Meldungen zu Vereinsmedien bearbeiten (PATCH + GET-Detail) - Club-Admins koennen Legal Hold auf Vereinsmedien (visibility != 'official') setzen - Neue Helfer: _assert_can_manage_report, _assert_can_set_legal_hold_from_report - set_legal_hold_from_report: Superadmin-Only aufgehoben fuer Vereinsebene Inbox UI: - Offene Meldungen (submitted/under_review) im Hauptbereich - Abgeschlossene Meldungen im kollabierbaren Archiv (standardmaessig zugeklappt) - Legal-Hold-Button sichtbar fuer Club-Admins bei nicht-offiziellen Medien - isClubAdmin + isPlatformAdmin aus OrgInboxContext verfuegbar version: 0.8.93 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9af28faa35
commit
5cf61289ec
|
|
@ -152,9 +152,60 @@ class ContentReportLegalHoldBody(BaseModel):
|
|||
|
||||
# ─── Hilfsfunktionen ────────────────────────────────────────────────────────
|
||||
|
||||
def _assert_platform_admin(role: Optional[str]) -> None:
|
||||
if not is_platform_admin(role):
|
||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admins koennen Meldungen bearbeiten")
|
||||
def _assert_can_manage_report(cur, role: Optional[str], pid: int, report: dict) -> None:
|
||||
"""Platform-Admins: jede Meldung. Club-Admin: nur Meldungen zu Medien ihres Vereins."""
|
||||
if is_platform_admin(role):
|
||||
return
|
||||
if report.get("target_type") != "media_asset":
|
||||
raise HTTPException(status_code=403, detail="Nur Plattform-Admins können Meldungen zu Übungen bearbeiten")
|
||||
cur.execute("SELECT club_id FROM media_assets WHERE id = %s", (int(report["target_id"]),))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
|
||||
club_id = r2d(row).get("club_id")
|
||||
if not club_id:
|
||||
raise HTTPException(status_code=403, detail="Kein Zugriff – Medium ohne Vereinszugehörigkeit")
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM club_members cm
|
||||
INNER JOIN club_member_roles r ON r.club_member_id = cm.id
|
||||
WHERE cm.profile_id = %s AND cm.club_id = %s
|
||||
AND cm.status = 'active' AND r.role_code = 'club_admin'
|
||||
""",
|
||||
(pid, club_id),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
raise HTTPException(status_code=403, detail="Kein Zugriff auf diese Meldung")
|
||||
|
||||
|
||||
def _assert_can_set_legal_hold_from_report(cur, role: Optional[str], pid: int, asset_id: int) -> None:
|
||||
"""Superadmin: immer. Club-Admin: nur für Vereinsmedien mit visibility != 'official'."""
|
||||
if is_superadmin(role):
|
||||
return
|
||||
cur.execute("SELECT club_id, visibility FROM media_assets WHERE id = %s", (asset_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
|
||||
asset = r2d(row)
|
||||
if asset.get("visibility") == "official":
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Legal Hold auf offiziellen Medien erfordert Superadmin-Rechte",
|
||||
)
|
||||
club_id = asset.get("club_id")
|
||||
if not club_id:
|
||||
raise HTTPException(status_code=403, detail="Legal Hold erfordert Superadmin-Rechte für Medien ohne Vereinszugehörigkeit")
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM club_members cm
|
||||
INNER JOIN club_member_roles r ON r.club_member_id = cm.id
|
||||
WHERE cm.profile_id = %s AND cm.club_id = %s
|
||||
AND cm.status = 'active' AND r.role_code = 'club_admin'
|
||||
""",
|
||||
(pid, club_id),
|
||||
)
|
||||
if cur.fetchone() is None:
|
||||
raise HTTPException(status_code=403, detail="Kein Zugriff – Superadmin oder Vereinsadmin des Vereins erforderlich")
|
||||
|
||||
|
||||
def _is_media_asset_visible_anonymous(cur, asset_id: int) -> bool:
|
||||
|
|
@ -651,8 +702,9 @@ def get_content_report(
|
|||
report_id: int,
|
||||
tenant: TenantContext = Depends(get_tenant_context),
|
||||
):
|
||||
"""Detail-Ansicht einer Meldung fuer Plattform-Admins."""
|
||||
_assert_platform_admin(tenant.global_role)
|
||||
"""Detail-Ansicht einer Meldung fuer Plattform-Admins und zustaendige Club-Admins."""
|
||||
pid = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -683,7 +735,9 @@ def get_content_report(
|
|||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Meldung nicht gefunden")
|
||||
return _report_row_to_dict(row)
|
||||
report_dict = _report_row_to_dict(row)
|
||||
_assert_can_manage_report(cur, role, pid, report_dict)
|
||||
return report_dict
|
||||
|
||||
|
||||
@router.patch("/content-reports/{report_id}")
|
||||
|
|
@ -695,9 +749,10 @@ def patch_content_report(
|
|||
"""
|
||||
Status und Bearbeitungsnotiz einer Meldung aktualisieren.
|
||||
Abschluss ohne Massnahme (resolved_no_action, rejected_invalid) erfordert resolution_note.
|
||||
Plattform-Admins: jede Meldung. Club-Admins: nur Meldungen zu Medien ihres Vereins.
|
||||
"""
|
||||
_assert_platform_admin(tenant.global_role)
|
||||
pid = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
if body.status in ("resolved_no_action", "rejected_invalid") and not (body.resolution_note or "").strip():
|
||||
raise HTTPException(
|
||||
|
|
@ -723,6 +778,7 @@ def patch_content_report(
|
|||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Meldung nicht gefunden")
|
||||
old_report = r2d(row)
|
||||
_assert_can_manage_report(cur, role, pid, old_report)
|
||||
old_status = old_report["status"]
|
||||
old_note = (old_report.get("resolution_note") or "").strip()
|
||||
|
||||
|
|
@ -802,13 +858,14 @@ def set_legal_hold_from_report(
|
|||
):
|
||||
"""
|
||||
Legal Hold (P-11) aus einer Meldung heraus setzen.
|
||||
Nur Superadmin. Nur fuer Meldungen mit target_type='media_asset'.
|
||||
Superadmin: immer. Club-Admin: nur fuer Vereinsmedien (visibility != 'official').
|
||||
Nur fuer Meldungen mit target_type='media_asset'.
|
||||
|
||||
Der Reason-Code wird automatisch aus dem report_reason der Meldung abgeleitet.
|
||||
Nach dem Setzen wird der Report-Status auf 'resolved_legal_hold' gesetzt.
|
||||
"""
|
||||
assert_superadmin_for_legal_hold(tenant.global_role)
|
||||
pid = tenant.profile_id
|
||||
role = tenant.global_role
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -828,6 +885,7 @@ def set_legal_hold_from_report(
|
|||
)
|
||||
|
||||
asset_id = int(report["target_id"])
|
||||
_assert_can_set_legal_hold_from_report(cur, role, pid, asset_id)
|
||||
reason_code = _REASON_TO_HOLD_CODE.get(report["report_reason"], "other")
|
||||
|
||||
# P-11-Service aufrufen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.92"
|
||||
APP_VERSION = "0.8.93"
|
||||
BUILD_DATE = "2026-05-11"
|
||||
DB_SCHEMA_VERSION = "20260511053"
|
||||
|
||||
|
|
@ -30,10 +30,20 @@ MODULE_VERSIONS = {
|
|||
"membership": "1.0.0",
|
||||
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
||||
"maturity_models": "1.4.0", # matrix_stack_bundle: vollständiger Katalog+Modelle+Bindings Export/Import
|
||||
"content_reports": "1.4.0", # P-13: Workflow-Reset (wieder öffnen), Kommentar-Audit-Log, PATCH-Verbesserungen
|
||||
"content_reports": "1.5.0", # P-13: Club-Admin Bearbeitung + Legal Hold (Vereinsebene), Archiv-Trennung
|
||||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.93",
|
||||
"date": "2026-05-11",
|
||||
"changes": [
|
||||
"Fix P-13: Club-Admins können Inhaltsmeldungen zu Vereinsmedien bearbeiten (PATCH, GET-Detail).",
|
||||
"Fix P-13: Club-Admins können Legal Hold auf Vereinsmedien (nicht 'official') aus Meldung heraus setzen.",
|
||||
"Fix P-13: Abgeschlossene Meldungen in der Inbox in kollabierbare Archiv-Sektion verschoben.",
|
||||
"Fix P-13: isClubAdmin + isPlatformAdmin im OrgInboxContext exponiert.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.92",
|
||||
"date": "2026-05-11",
|
||||
|
|
|
|||
|
|
@ -115,8 +115,10 @@ export function OrgInboxProvider({ user, children }) {
|
|||
canAccessOrgInbox: canAccess,
|
||||
canAccessContentReports: canAccessReports,
|
||||
isSuperadmin: user?.role === 'superadmin',
|
||||
isPlatformAdmin: user?.role === 'admin' || user?.role === 'superadmin',
|
||||
isClubAdmin: activeClubMemberships(user?.clubs || []).some((c) => (c.roles || []).includes('club_admin')),
|
||||
}),
|
||||
[items, contentReports, contentReportsError, refresh, canAccess, canAccessReports, user?.role]
|
||||
[items, contentReports, contentReportsError, refresh, canAccess, canAccessReports, user?.role, user?.clubs]
|
||||
)
|
||||
|
||||
return <OrgInboxContext.Provider value={value}>{children}</OrgInboxContext.Provider>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ function WorkflowBar({ status }) {
|
|||
)
|
||||
}
|
||||
|
||||
function ReportDetailModal({ report, onClose, onRefresh, isSuperadmin }) {
|
||||
function ReportDetailModal({ report, onClose, onRefresh, isSuperadmin, isClubAdmin }) {
|
||||
const [resolutionNote, setResolutionNote] = useState(report.resolution_note || '')
|
||||
const [legalHoldNote, setLegalHoldNote] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
|
@ -279,13 +279,13 @@ function ReportDetailModal({ report, onClose, onRefresh, isSuperadmin }) {
|
|||
Meldung abweisen
|
||||
</button>
|
||||
</div>
|
||||
{isSuperadmin && !showLegalHoldForm && (
|
||||
{(isSuperadmin || (isClubAdmin && report.target_visibility !== 'official')) && !showLegalHoldForm && (
|
||||
<button
|
||||
type="button" className="btn"
|
||||
style={{ background: 'rgba(216,90,48,0.12)', color: 'var(--danger)', border: '1px solid var(--danger)' }}
|
||||
onClick={() => setShowLegalHoldForm(true)}
|
||||
>
|
||||
Legal Hold setzen (Superadmin)
|
||||
Legal Hold setzen {isSuperadmin ? '(Superadmin)' : '(Vereinsadmin)'}
|
||||
</button>
|
||||
)}
|
||||
{isSuperadmin && showLegalHoldForm && (
|
||||
|
|
@ -325,6 +325,8 @@ export default function InboxPage() {
|
|||
canAccessOrgInbox,
|
||||
canAccessContentReports,
|
||||
isSuperadmin,
|
||||
isPlatformAdmin,
|
||||
isClubAdmin,
|
||||
refreshOrgInbox,
|
||||
inboxJoinRequests,
|
||||
contentReports,
|
||||
|
|
@ -334,6 +336,7 @@ export default function InboxPage() {
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [acceptModal, setAcceptModal] = useState(null)
|
||||
const [reportModal, setReportModal] = useState(null)
|
||||
const [showArchive, setShowArchive] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!canAccessOrgInbox && !canAccessContentReports) {
|
||||
|
|
@ -471,89 +474,108 @@ export default function InboxPage() {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* Abschnitt 2: Inhaltsmeldungen (nur Plattform-Admins) */}
|
||||
{canAccessContentReports && (
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text2)' }}>
|
||||
Inhaltsmeldungen
|
||||
{contentReportCount > 0 && (
|
||||
<span
|
||||
style={{
|
||||
background: 'var(--danger)',
|
||||
color: '#fff',
|
||||
borderRadius: '12px',
|
||||
padding: '1px 8px',
|
||||
fontSize: '0.75rem',
|
||||
marginLeft: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{contentReportCount} neu
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{/* Abschnitt 2: Inhaltsmeldungen */}
|
||||
{canAccessContentReports && (() => {
|
||||
const openReports = contentReports.filter((r) => r.status === 'submitted' || r.status === 'under_review')
|
||||
const archivedReports = contentReports.filter((r) => r.status !== 'submitted' && r.status !== 'under_review')
|
||||
|
||||
{contentReportsError ? (
|
||||
<div className="card" style={{ padding: '1.25rem', borderLeft: '3px solid var(--danger)' }}>
|
||||
<p style={{ margin: 0, color: 'var(--danger)', fontWeight: 600, fontSize: '0.88rem' }}>
|
||||
Fehler beim Laden: {contentReportsError}
|
||||
</p>
|
||||
<button type="button" className="btn btn-secondary" style={{ marginTop: '0.5rem', fontSize: '0.82rem' }} onClick={load}>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
) : contentReports.length === 0 ? (
|
||||
<div className="card" style={{ padding: '1.25rem' }}>
|
||||
<p style={{ margin: 0 }} className="muted">Keine Inhaltsmeldungen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{contentReports.map((rep) => (
|
||||
<div
|
||||
key={rep.id}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1rem 1.25rem',
|
||||
cursor: 'pointer',
|
||||
borderLeft: rep.priority === 'high' ? '3px solid var(--danger)' : '3px solid transparent',
|
||||
}}
|
||||
onClick={() => setReportModal(rep)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||
<div>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
Meldung #{rep.id}
|
||||
<PriorityBadge priority={rep.priority} />
|
||||
</span>
|
||||
<span className="muted" style={{ marginLeft: '0.75rem', fontSize: '0.85rem' }}>
|
||||
{rep.target_type === 'media_asset' ? 'Medium' : 'Übung'} #{rep.target_id}
|
||||
{rep.target_filename || rep.target_exercise_name ? ` – ${rep.target_filename || rep.target_exercise_name}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
color: STATUS_COLORS[rep.status],
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{STATUS_LABELS[rep.status] || rep.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: '0.85rem', marginTop: '0.3rem' }}>
|
||||
{REASON_LABELS[rep.report_reason] || rep.report_reason}
|
||||
{' · '}
|
||||
{rep.reporter_name}
|
||||
{rep.reporter_profile_id ? ` (Profil #${rep.reporter_profile_id})` : ' (anonym)'}
|
||||
{' · '}
|
||||
{formatWhen(rep.submitted_at || rep.created_at)}
|
||||
</div>
|
||||
function ReportCard({ rep }) {
|
||||
return (
|
||||
<div
|
||||
key={rep.id}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1rem 1.25rem',
|
||||
cursor: 'pointer',
|
||||
borderLeft: rep.priority === 'high' ? '3px solid var(--danger)' : '3px solid transparent',
|
||||
opacity: rep.status !== 'submitted' && rep.status !== 'under_review' ? 0.75 : 1,
|
||||
}}
|
||||
onClick={() => setReportModal(rep)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||
<div>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
Meldung #{rep.id}
|
||||
<PriorityBadge priority={rep.priority} />
|
||||
</span>
|
||||
<span className="muted" style={{ marginLeft: '0.75rem', fontSize: '0.85rem' }}>
|
||||
{rep.target_type === 'media_asset' ? 'Medium' : 'Übung'} #{rep.target_id}
|
||||
{rep.target_filename || rep.target_exercise_name ? ` – ${rep.target_filename || rep.target_exercise_name}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<span style={{ fontSize: '0.78rem', fontWeight: 500, color: STATUS_COLORS[rep.status], whiteSpace: 'nowrap' }}>
|
||||
{STATUS_LABELS[rep.status] || rep.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="muted" style={{ fontSize: '0.85rem', marginTop: '0.3rem' }}>
|
||||
{REASON_LABELS[rep.report_reason] || rep.report_reason}
|
||||
{' · '}
|
||||
{rep.reporter_name}
|
||||
{rep.reporter_profile_id ? ` (Profil #${rep.reporter_profile_id})` : ' (anonym)'}
|
||||
{' · '}
|
||||
{formatWhen(rep.submitted_at || rep.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text2)' }}>
|
||||
Inhaltsmeldungen
|
||||
{contentReportCount > 0 && (
|
||||
<span style={{ background: 'var(--danger)', color: '#fff', borderRadius: '12px', padding: '1px 8px', fontSize: '0.75rem', marginLeft: '0.5rem' }}>
|
||||
{contentReportCount} neu
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{contentReportsError ? (
|
||||
<div className="card" style={{ padding: '1.25rem', borderLeft: '3px solid var(--danger)' }}>
|
||||
<p style={{ margin: 0, color: 'var(--danger)', fontWeight: 600, fontSize: '0.88rem' }}>
|
||||
Fehler beim Laden: {contentReportsError}
|
||||
</p>
|
||||
<button type="button" className="btn btn-secondary" style={{ marginTop: '0.5rem', fontSize: '0.82rem' }} onClick={load}>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{openReports.length === 0 ? (
|
||||
<div className="card" style={{ padding: '1.25rem', marginBottom: '0.75rem' }}>
|
||||
<p style={{ margin: 0 }} className="muted">Keine offenen Inhaltsmeldungen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginBottom: '0.75rem' }}>
|
||||
{openReports.map((rep) => <ReportCard key={rep.id} rep={rep} />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{archivedReports.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer', padding: '0.4rem 0',
|
||||
color: 'var(--text2)', fontSize: '0.88rem', display: 'flex', alignItems: 'center', gap: '0.35rem',
|
||||
}}
|
||||
onClick={() => setShowArchive((v) => !v)}
|
||||
>
|
||||
<span style={{ fontSize: '0.75rem' }}>{showArchive ? '▼' : '▶'}</span>
|
||||
Archiv ({archivedReports.length} abgeschlossene Meldung{archivedReports.length !== 1 ? 'en' : ''})
|
||||
</button>
|
||||
{showArchive && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||
{archivedReports.map((rep) => <ReportCard key={rep.id} rep={rep} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -642,6 +664,7 @@ export default function InboxPage() {
|
|||
<ReportDetailModal
|
||||
report={reportModal}
|
||||
isSuperadmin={isSuperadmin}
|
||||
isClubAdmin={isClubAdmin && !isPlatformAdmin}
|
||||
onClose={() => setReportModal(null)}
|
||||
onRefresh={load}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.92"
|
||||
export const APP_VERSION = "0.8.93"
|
||||
export const BUILD_DATE = "2026-05-11"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
@ -27,8 +27,8 @@ export const PAGE_VERSIONS = {
|
|||
ExerciseMediaEmbed: "1.1.0", // P-11: Legal-Hold-Placeholder statt Datei
|
||||
ExerciseMediaThumbTile: "1.1.0", // P-11: Legal-Hold-Kachel statt Datei-Vorschau
|
||||
ExerciseAttachmentMediaStrip: "1.2.0", // P-13: MediaPreviewModal (geteilt) + Melden im Viewer
|
||||
InboxPage: "2.2.0", // P-13: Workflow-Balken, Wieder-öffnen, Kommentar, Fehleranzeigezeige
|
||||
OrgInboxContext: "1.2.0", // P-13: contentReportsError exposed
|
||||
InboxPage: "2.3.0", // P-13: Archiv-Trennung offen/abgeschlossen; Club-Admin Legal-Hold-Button
|
||||
OrgInboxContext: "1.3.0", // P-13: isClubAdmin + isPlatformAdmin exposed
|
||||
MediaPreviewModal: "1.0.0", // P-13: geteilter Medienvorschau-Dialog (Melden + Bearbeiten optional)
|
||||
ReportContentModal: "1.2.0", // P-13: onSuccess callback fuer Badge-Update
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user