Split dashboard InboxPanel card renderer

This commit is contained in:
ejclaw
2026-04-28 14:58:06 +09:00
parent eb13c3d332
commit 77ad6fa51d

View File

@@ -77,101 +77,37 @@ function inboxTargetHref(item: InboxItem): string | null {
return null; return null;
} }
export function InboxPanel({ interface InboxCardProps {
overview, inboxActionKey: InboxActionKey | null;
tasks, item: InboxItem;
locale: Locale;
onInboxAction: (item: InboxItem, action: DashboardInboxAction) => void;
onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void;
taskActionKey: string | null;
tasks: DashboardTask[];
t: Messages;
}
function InboxCard({
inboxActionKey,
item,
locale, locale,
onInboxAction, onInboxAction,
onTaskAction, onTaskAction,
inboxActionKey,
taskActionKey, taskActionKey,
tasks,
t, t,
}: InboxPanelProps) { }: InboxCardProps) {
const [filter, setFilter] = useState<InboxFilter>('all');
const items = overview.inbox ?? [];
const counts = useMemo(() => {
const next: Record<InboxFilter, number> = {
all: items.length,
'pending-room': 0,
'reviewer-request': 0,
approval: 0,
'arbiter-request': 0,
'ci-failure': 0,
mention: 0,
};
for (const item of items) next[item.kind] += 1;
return next;
}, [items]);
const filteredItems =
filter === 'all' ? items : items.filter((item) => item.kind === filter);
const severityCounts = items.reduce(
(acc, item) => {
acc[item.severity] += 1;
return acc;
},
{ error: 0, warn: 0, info: 0 },
);
if (items.length === 0) {
return <EmptyState>{t.inbox.empty}</EmptyState>;
}
return (
<div className="inbox-board">
<section className="inbox-summary" aria-label={t.inbox.summary}>
<div>
<span>{t.inbox.total}</span>
<strong>{items.length}</strong>
</div>
<div>
<span>{t.inbox.severity.error}</span>
<strong>{severityCounts.error}</strong>
</div>
<div>
<span>{t.inbox.severity.warn}</span>
<strong>{severityCounts.warn}</strong>
</div>
<div>
<span>{t.inbox.severity.info}</span>
<strong>{severityCounts.info}</strong>
</div>
</section>
<div className="inbox-filters" aria-label={t.inbox.filters}>
{INBOX_FILTERS.map((item) => {
if (item !== 'all' && counts[item] === 0) return null;
const label = item === 'all' ? t.inbox.all : t.inbox.kinds[item];
return (
<button
aria-pressed={filter === item}
className={filter === item ? 'is-active' : undefined}
key={item}
onClick={() => setFilter(item)}
type="button"
>
{label}
<span>{counts[item]}</span>
</button>
);
})}
</div>
<div className="inbox-list" aria-label={t.inbox.cardsAria}>
{filteredItems.map((item) => {
const href = inboxTargetHref(item); const href = inboxTargetHref(item);
const linkedTask = const linkedTask =
item.source === 'scheduled-task' && item.taskId item.source === 'scheduled-task' && item.taskId
? tasks.find((task) => task.id === item.taskId) ? tasks.find((task) => task.id === item.taskId)
: undefined; : undefined;
const linkedTaskActions = linkedTask const linkedTaskActions = linkedTask ? taskActionsFor(linkedTask) : [];
? taskActionsFor(linkedTask)
: [];
const inboxActions = inboxActionsFor(item); const inboxActions = inboxActionsFor(item);
return ( return (
<article <article className={`inbox-card inbox-${item.severity}`}>
className={`inbox-card inbox-${item.severity}`}
key={item.id}
>
<div className="inbox-card-head"> <div className="inbox-card-head">
<div> <div>
<span className="eyebrow">{t.inbox.kinds[item.kind]}</span> <span className="eyebrow">{t.inbox.kinds[item.kind]}</span>
@@ -256,8 +192,102 @@ export function InboxPanel({
) : null} ) : null}
</article> </article>
); );
}
export function InboxPanel({
overview,
tasks,
locale,
onInboxAction,
onTaskAction,
inboxActionKey,
taskActionKey,
t,
}: InboxPanelProps) {
const [filter, setFilter] = useState<InboxFilter>('all');
const items = overview.inbox ?? [];
const counts = useMemo(() => {
const next: Record<InboxFilter, number> = {
all: items.length,
'pending-room': 0,
'reviewer-request': 0,
approval: 0,
'arbiter-request': 0,
'ci-failure': 0,
mention: 0,
};
for (const item of items) next[item.kind] += 1;
return next;
}, [items]);
const filteredItems =
filter === 'all' ? items : items.filter((item) => item.kind === filter);
const severityCounts = items.reduce(
(acc, item) => {
acc[item.severity] += 1;
return acc;
},
{ error: 0, warn: 0, info: 0 },
);
if (items.length === 0) {
return <EmptyState>{t.inbox.empty}</EmptyState>;
}
return (
<div className="inbox-board">
<section className="inbox-summary" aria-label={t.inbox.summary}>
<div>
<span>{t.inbox.total}</span>
<strong>{items.length}</strong>
</div>
<div>
<span>{t.inbox.severity.error}</span>
<strong>{severityCounts.error}</strong>
</div>
<div>
<span>{t.inbox.severity.warn}</span>
<strong>{severityCounts.warn}</strong>
</div>
<div>
<span>{t.inbox.severity.info}</span>
<strong>{severityCounts.info}</strong>
</div>
</section>
<div className="inbox-filters" aria-label={t.inbox.filters}>
{INBOX_FILTERS.map((item) => {
if (item !== 'all' && counts[item] === 0) return null;
const label = item === 'all' ? t.inbox.all : t.inbox.kinds[item];
return (
<button
aria-pressed={filter === item}
className={filter === item ? 'is-active' : undefined}
key={item}
onClick={() => setFilter(item)}
type="button"
>
{label}
<span>{counts[item]}</span>
</button>
);
})} })}
</div> </div>
<div className="inbox-list" aria-label={t.inbox.cardsAria}>
{filteredItems.map((item) => (
<InboxCard
inboxActionKey={inboxActionKey}
item={item}
key={item.id}
locale={locale}
onInboxAction={onInboxAction}
onTaskAction={onTaskAction}
taskActionKey={taskActionKey}
tasks={tasks}
t={t}
/>
))}
</div>
</div> </div>
); );
} }