fix(iace): confirmation dialog for ungrouping + undo/regroup
Build + Deploy / build-admin-compliance (push) Successful in 1m53s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 9s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 12s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m40s
CI / dep-audit (push) Has been skipped
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 44s
CI / test-python-backend (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m29s
Build + Deploy / build-admin-compliance (push) Successful in 1m53s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 9s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 12s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m40s
CI / dep-audit (push) Has been skipped
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 44s
CI / test-python-backend (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m29s
- X button replaced with confirmation dialog: "Als eigenen Punkt fuehren" / "Abbrechen" - Dialog explains the action and that it's reversible - Ungrouped items show orange "Zurueck in Block" button - Info bar shows count of ungrouped items + "alle zuruecksetzen" link - No destructive action without user confirmation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
-10
@@ -37,6 +37,7 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
|
||||
const [blocks, setBlocks] = useState<BlockData[]>([])
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const [ungrouped, setUngrouped] = useState<Record<string, boolean>>({})
|
||||
const [pendingAction, setPendingAction] = useState<{ childId: string; childName: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazard-blocks`)
|
||||
@@ -110,14 +111,50 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
|
||||
|
||||
const handleUngroup = (childId: string) => {
|
||||
setUngrouped(prev => ({ ...prev, [childId]: true }))
|
||||
setPendingAction(null)
|
||||
}
|
||||
|
||||
const handleRegroup = (childId: string) => {
|
||||
setUngrouped(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[childId]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
// Count blocks with children
|
||||
const blockCount = blocks.filter(b => b.children.length > 0).length
|
||||
const coveredCount = Object.values(blockMap).filter(b => b.isChild && b.isCovered).length
|
||||
const ungroupedCount = Object.keys(ungrouped).length
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Confirmation dialog */}
|
||||
{pendingAction && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 p-5 max-w-md w-full mx-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Gefaehrdung aus Block entfernen?</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||
<strong>{pendingAction.childName}</strong>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Der Punkt wird als eigenstaendige Gefaehrdung gefuehrt und muss separat bewertet werden.
|
||||
Sie koennen ihn jederzeit ueber "Zurueck in Block" wieder zuordnen.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleUngroup(pendingAction.childId)}
|
||||
className="flex-1 px-3 py-2 text-xs font-medium bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
Als eigenen Punkt fuehren
|
||||
</button>
|
||||
<button onClick={() => setPendingAction(null)}
|
||||
className="flex-1 px-3 py-2 text-xs font-medium bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Block info bar */}
|
||||
{blockCount > 0 && (
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-xs">
|
||||
@@ -129,9 +166,12 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
|
||||
{coveredCount} Kinder durch Mutter abgedeckt
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-500">
|
||||
Klick auf Block-Icon zum Auf-/Zuklappen. Rechtsklick oder Aktion zum Entgruppieren.
|
||||
</span>
|
||||
{ungroupedCount > 0 && (
|
||||
<button onClick={() => setUngrouped({})}
|
||||
className="text-orange-600 hover:text-orange-700 underline">
|
||||
{ungroupedCount} entgruppiert — alle zuruecksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -173,17 +213,26 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
|
||||
</button>
|
||||
)}
|
||||
{isChild && (
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<div className="w-1 h-1 rounded-full bg-gray-300" />
|
||||
<button onClick={() => handleUngroup(h.id)}
|
||||
className="w-4 h-4 flex items-center justify-center rounded hover:bg-red-100 text-gray-400 hover:text-red-500 transition-colors"
|
||||
title="Aus Block entfernen">
|
||||
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<div className="flex items-center justify-center">
|
||||
<button onClick={() => setPendingAction({ childId: h.id, childName: h.name })}
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-orange-100 text-gray-300 hover:text-orange-500 transition-colors"
|
||||
title="Aus Block entfernen (mit Bestaetigung)">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Show regroup button for ungrouped items */}
|
||||
{!isParent && !isChild && ungrouped[h.id] && (
|
||||
<button onClick={() => handleRegroup(h.id)}
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-green-100 text-orange-400 hover:text-green-600 transition-colors"
|
||||
title="Zurueck in Block">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
{/* Name */}
|
||||
<td className={`px-3 py-2 ${isChild ? 'pl-8' : ''}`}>
|
||||
|
||||
@@ -467,15 +467,36 @@ async def click_button(page: Page, selector: str, timeout: int = 5000) -> bool:
|
||||
text_pattern = selector[len("shadow-click:"):]
|
||||
return await _click_in_shadow_dom(page, text_pattern)
|
||||
|
||||
# 1. Try main document
|
||||
try:
|
||||
locator = page.locator(selector).first
|
||||
await locator.wait_for(state="visible", timeout=timeout)
|
||||
await locator.click()
|
||||
return True
|
||||
except Exception:
|
||||
# Fallback: try Shadow DOM click with selector text
|
||||
# Extract button text from selector like 'button:has-text("Accept all")'
|
||||
if ':has-text("' in selector:
|
||||
text = selector.split(':has-text("')[1].rstrip('")')
|
||||
return await _click_in_shadow_dom(page, text)
|
||||
return False
|
||||
pass
|
||||
|
||||
# 2. Fallback: try inside iframes (Sourcepoint, Quantcast, etc.)
|
||||
try:
|
||||
for iframe_sel in [
|
||||
"iframe[id^='sp_message']", # Sourcepoint
|
||||
"iframe[id*='consent']",
|
||||
"iframe[title*='SP Consent']",
|
||||
"iframe[title*='consent']",
|
||||
]:
|
||||
try:
|
||||
frame = page.frame_locator(iframe_sel)
|
||||
btn = frame.locator(selector).first
|
||||
if await btn.count() > 0:
|
||||
await btn.click(timeout=timeout)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Fallback: Shadow DOM
|
||||
if ':has-text("' in selector:
|
||||
text = selector.split(':has-text("')[1].rstrip('")')
|
||||
return await _click_in_shadow_dom(page, text)
|
||||
return False
|
||||
|
||||
@@ -81,20 +81,32 @@ async def try_dismiss_consent_banner(page: Page) -> bool:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 3) Sourcepoint (iframe-based CMP, used by Spiegel, Zeit, etc.)
|
||||
# 3) Sourcepoint / iframe-based CMPs (Spiegel, Zeit, etc.)
|
||||
# Search ALL iframes for consent buttons — Sourcepoint generates dynamic IDs
|
||||
try:
|
||||
sp_div = await page.query_selector("div[id^='sp_message']")
|
||||
if sp_div:
|
||||
# Sourcepoint renders in an iframe inside sp_message_container
|
||||
sp_iframe = page.frame_locator("iframe[id^='sp_message']")
|
||||
accept_btn = sp_iframe.locator(".sp_choice_type_11").first
|
||||
if await accept_btn.count() > 0:
|
||||
await accept_btn.click(timeout=5000)
|
||||
logger.info("Dismissed Sourcepoint consent banner (iframe)")
|
||||
await page.wait_for_timeout(3000)
|
||||
return True
|
||||
for frame in page.frames:
|
||||
if frame == page.main_frame:
|
||||
continue
|
||||
try:
|
||||
# Sourcepoint accept button
|
||||
sp_btn = frame.locator(".sp_choice_type_11").first
|
||||
if await sp_btn.count() > 0 and await sp_btn.is_visible():
|
||||
await sp_btn.click(timeout=5000)
|
||||
logger.info("Dismissed Sourcepoint consent (iframe: %s)", frame.url[:80])
|
||||
await page.wait_for_timeout(3000)
|
||||
return True
|
||||
# Generic accept text in iframe
|
||||
for text in ["Akzeptieren", "Zustimmen", "Accept all", "Alle akzeptieren"]:
|
||||
btn = frame.locator(f'button:has-text("{text}")').first
|
||||
if await btn.count() > 0 and await btn.is_visible():
|
||||
await btn.click(timeout=3000)
|
||||
logger.info("Dismissed iframe consent via '%s'", text)
|
||||
await page.wait_for_timeout(3000)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug("Sourcepoint dismiss attempt: %s", e)
|
||||
logger.debug("Iframe consent dismiss: %s", e)
|
||||
|
||||
# 4) Use banner_detector CMP selectors as fallback
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user