727d86614e
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import React from 'react';
|
|
import type { MessageBlock } from '@/store/commStore';
|
|
|
|
interface ActionCardBlockProps {
|
|
block: MessageBlock;
|
|
}
|
|
|
|
interface ActionItem {
|
|
label: string;
|
|
action: string;
|
|
type?: 'primary' | 'secondary';
|
|
}
|
|
|
|
const ActionCardBlock: React.FC<ActionCardBlockProps> = ({ block }) => {
|
|
const { title, body, actions } = block.block_data;
|
|
|
|
const cardTitle: string = title || '';
|
|
const cardBody: string = body || '';
|
|
const actionList: ActionItem[] = Array.isArray(actions) ? actions : [];
|
|
|
|
const handleActionClick = (action: ActionItem) => {
|
|
if (action.action === 'dismiss') {
|
|
// Frontend handles dismiss — no-op here, parent component can wire up
|
|
return;
|
|
}
|
|
// Validate URL — only allow http: and https: protocols to prevent javascript: URLs
|
|
const url = action.action;
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;
|
|
} catch {
|
|
return; // Invalid URL
|
|
}
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
};
|
|
|
|
return (
|
|
<div className="border border-secondary-200 rounded-lg p-4 shadow-sm bg-white space-y-2">
|
|
{cardTitle && (
|
|
<h4 className="text-sm font-semibold text-secondary-800">{cardTitle}</h4>
|
|
)}
|
|
{cardBody && (
|
|
<p className="text-sm text-secondary-600">{cardBody}</p>
|
|
)}
|
|
{actionList.length > 0 && (
|
|
<div className="flex flex-wrap gap-2 pt-1">
|
|
{actionList.map((action, idx) => {
|
|
const isPrimary = action.type !== 'secondary';
|
|
return (
|
|
<button
|
|
key={idx}
|
|
onClick={() => handleActionClick(action)}
|
|
className={
|
|
isPrimary
|
|
? 'px-3 py-1.5 text-sm rounded-lg bg-primary-600 text-white hover:bg-primary-700 transition-colors'
|
|
: 'px-3 py-1.5 text-sm rounded-lg bg-secondary-100 text-secondary-700 hover:bg-secondary-200 transition-colors'
|
|
}
|
|
>
|
|
{action.label || 'Aktion'}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ActionCardBlock;
|