前端:任务详情展示审批/驳回记录 + 任务行 ID 不被长标题挤掉 + i18n 五语 (tsk_EmZ6vjtVbrOk)
- TaskDetail 渲染 _raw.approvals:闸名·通过/驳回 + 审批人 + 时间, 驳回用红色边框+红底高亮块突出 reason(t.rejectReason 标签), 通过用绿色、reason 走低调 ↳ 行;无审批记录不渲染该分区。 - 安全:reason/actor/gate 全部走 React 文本节点(默认转义), 不经 MaestroRichDoc/innerHTML,避免同源存储型 XSS。 - TaskRow:taskid 拆为独立 flex:none span,title 加 minWidth:0, 长标题省略号截断时 id 完整不被挤掉;ArchiveRow 同步处理保持一致。 - i18n:zh/en/es/ja/fr 五语新增 rejectReason 键。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -227,9 +227,10 @@ function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleO
|
||||
}}>
|
||||
{kids.length ? '▶' : '·'}
|
||||
</span>
|
||||
<span style={{ fontWeight: 500, color: ctx ? 'var(--muted)' : 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{task.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{task.id}</span>
|
||||
<span style={{ flex: '0 1 auto', minWidth: 0, fontWeight: 500, color: ctx ? 'var(--muted)' : 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{task.title}
|
||||
</span>
|
||||
<span style={{ flex: 'none', color: 'var(--faint)', fontSize: 10.5 }}>{task.id}</span>
|
||||
<span style={{ flex: 1, minWidth: 8 }}></span>
|
||||
{task.deps ? (
|
||||
<span style={{ flex: 'none', fontSize: 10.5, letterSpacing: '.06em', color: 'var(--amber)', border: '1px dashed var(--amber-dim)', borderRadius: 3, padding: '1px 7px', lineHeight: 1.5, whiteSpace: 'nowrap' }}>
|
||||
@@ -384,6 +385,46 @@ function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{/* 审批记录:读 _raw.approvals,按记录列出闸名·通过/驳回 + 审批人 + 时间;驳回突出 reason。
|
||||
安全:reason/actor/gate 全部走 React 文本节点(默认转义),绝不经 MaestroRichDoc / innerHTML,避免同源存储型 XSS。 */}
|
||||
{(() => {
|
||||
const approvals = (task._raw && task._raw.approvals) || [];
|
||||
if (!approvals.length) return null;
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{t.approvalsLabel}</div>
|
||||
<div style={{ display: 'grid', gap: 6 }}>
|
||||
{approvals.map((a, i) => {
|
||||
const rej = a.action === 'reject';
|
||||
const at = (a.at || '').replace('T', ' ').slice(0, 19);
|
||||
const gate = (t.gates && t.gates[a.gate]) || a.gate;
|
||||
return (
|
||||
<div key={i} style={{
|
||||
fontSize: 12, padding: '6px 10px', borderRadius: 4,
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line-soft)',
|
||||
borderLeft: '2px solid ' + (rej ? 'var(--red-dim)' : 'var(--green-dim)'),
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ color: rej ? 'var(--red)' : 'var(--green)', fontWeight: 700, flex: 'none' }}>
|
||||
{gate} · {rej ? t.rejected : t.accepted}
|
||||
</span>
|
||||
<span style={{ color: 'var(--faint)', fontSize: 11 }}>{t.approver} {a.actor} · {at}</span>
|
||||
</div>
|
||||
{rej && a.reason ? (
|
||||
<div style={{ marginTop: 5, padding: '5px 9px', borderRadius: 3,
|
||||
background: 'rgba(240,100,100,.08)', borderLeft: '2px solid var(--red-dim)', color: 'var(--amber)' }}>
|
||||
<span style={{ color: 'var(--red)', fontWeight: 600, marginRight: 6 }}>{t.rejectReason}</span>{a.reason}
|
||||
</div>
|
||||
) : a.reason ? (
|
||||
<div style={{ marginTop: 4, color: 'var(--muted)' }}>↳ {a.reason}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user