修复画布任务列表完成展示

阻止画布生成完成快照自动展开生成器对话框
已完成任务按完成时间排序
恢复任务列表状态、时间和进度展示

(cherry picked from commit febe4811768b1bce73dbc37ca412b3834da2d699)
This commit is contained in:
2026-06-30 14:05:25 +08:00
parent c556d42d22
commit dbb651e1f4
5 changed files with 81 additions and 39 deletions
@@ -3729,7 +3729,7 @@ fn apply_editor_canvas_generation_items(
&& let Some(dialog) = item.get_mut("dialog").and_then(Value::as_object_mut)
{
dialog.insert("status".to_string(), json!("idle"));
dialog.insert("composerOpen".to_string(), json!(true));
dialog.insert("composerOpen".to_string(), json!(false));
if let Some(completed_layer_id) = completed_layer_id.as_deref() {
dialog.insert("generatedLayerId".to_string(), json!(completed_layer_id));
}
@@ -5482,7 +5482,7 @@ mod tests {
}
#[test]
fn editor_canvas_generation_completion_inserts_result_layer_and_marks_dialog_idle() {
fn editor_canvas_generation_completion_inserts_result_layer_and_keeps_composer_closed() {
let layers = json!([
{
"layerId": "layer-existing",
@@ -5585,7 +5585,7 @@ mod tests {
.and_then(|item| item.get("dialog"))
.expect("dialog should remain");
assert_eq!(dialog["status"], json!("idle"));
assert_eq!(dialog["composerOpen"], json!(true));
assert_eq!(dialog["composerOpen"], json!(false));
assert_eq!(
dialog["generatedLayerId"],
json!("layer-editor-resource-editor-resource-character")
@@ -592,10 +592,8 @@ fn list_external_generation_jobs_tx(
external_generation_job_sort_bucket(left)
.cmp(&external_generation_job_sort_bucket(right))
.then_with(|| {
right
.updated_at
.to_micros_since_unix_epoch()
.cmp(&left.updated_at.to_micros_since_unix_epoch())
external_generation_job_sort_time_micros(right)
.cmp(&external_generation_job_sort_time_micros(left))
})
.then_with(|| left.job_id.cmp(&right.job_id))
});
@@ -970,6 +968,16 @@ fn external_generation_job_sort_bucket(row: &ExternalGenerationJob) -> u8 {
}
}
fn external_generation_job_sort_time_micros(row: &ExternalGenerationJob) -> i64 {
if is_external_generation_job_terminal(row) {
return row
.completed_at
.unwrap_or(row.updated_at)
.to_micros_since_unix_epoch();
}
row.updated_at.to_micros_since_unix_epoch()
}
fn normalize_external_generation_job_status_filter(statuses: &[String]) -> Vec<&'static str> {
statuses
.iter()
@@ -1252,6 +1260,23 @@ mod tests {
);
}
#[test]
fn task_list_sort_time_uses_completion_time_for_terminal_jobs() {
let mut completed = external_generation_job_fixture(EXTERNAL_GENERATION_STATUS_COMPLETED);
completed.completed_at = Some(micros(2_000));
completed.updated_at = micros(9_000);
let mut older_completed =
external_generation_job_fixture(EXTERNAL_GENERATION_STATUS_COMPLETED);
older_completed.completed_at = Some(micros(1_000));
older_completed.updated_at = micros(10_000);
assert!(external_generation_job_sort_time_micros(&completed) > 1_000);
assert!(
external_generation_job_sort_time_micros(&completed)
> external_generation_job_sort_time_micros(&older_completed)
);
}
#[test]
fn task_list_status_filter_accepts_public_queued_alias() {
assert_eq!(
@@ -90,6 +90,15 @@ describe('ImageCanvasTaskSidebarView', () => {
}),
]
: [
createExternalTask({
jobId: 'older-done-task',
requestLabel: '较早完成',
requestPrompt: '较早完成任务',
status: 'completed',
startedAt: new Date(Date.now() - 9000).toISOString(),
completedAt: new Date(Date.now() - 8000).toISOString(),
updatedAt: new Date(Date.now() - 500).toISOString(),
}),
createExternalTask({
jobId: 'done-task',
requestLabel: '视频生成',
@@ -151,8 +160,14 @@ describe('ImageCanvasTaskSidebarView', () => {
const completedTab = screen.getByRole('tab', { name: //u });
fireEvent.click(completedTab);
expect(await screen.findByText('视频生成')).toBeTruthy();
const completedTitles = screen
.getAllByRole('button')
.map((button) => button.textContent ?? '')
.filter((text) => text.includes('视频生成') || text.includes('较早完成'));
expect(completedTitles[0]).toContain('视频生成');
expect(completedTitles[1]).toContain('较早完成');
expect(screen.getByText(//u)).toBeTruthy();
expect(screen.queryByText(//u)).not.toBeNull();
expect(screen.queryAllByText(//u).length).toBeGreaterThan(0);
expect(screen.queryByText(//u)).toBeNull();
fireEvent.wheel(screen.getByLabelText('画布任务列表'), { deltaY: 120 });
@@ -220,9 +235,11 @@ describe('ImageCanvasTaskSidebarView', () => {
/>,
);
expect(await screen.findByText('生成图片')).toBeTruthy();
expect(screen.getByText(/ 35% · 2\/4 /u)).toBeTruthy();
expect(await screen.findByText('图片画布生成图片')).toBeTruthy();
expect(screen.getByText(//u)).toBeTruthy();
expect(screen.getByText(/35% · 2\/4 /u)).toBeTruthy();
expect(screen.getByText(/ 1/u)).toBeTruthy();
expect(screen.queryByText(//u)).toBeNull();
expect(screen.queryByText(//u)).toBeNull();
await waitFor(() =>
expect(
@@ -101,8 +101,13 @@ function externalTaskDetail(
);
}
const phaseLabel = task.phaseLabel.trim();
const phaseDetail = task.phaseDetail.trim();
return phaseLabel || phaseDetail || task.requestLabel.trim() || task.jobKind;
const requestLabel = task.requestLabel.trim();
const prompt = normalizeTaskPrompt(task.requestPrompt);
return (
(prompt && prompt !== requestLabel ? prompt : null) ||
(phaseLabel && phaseLabel !== requestLabel ? phaseLabel : null) ||
''
);
}
function externalTaskProgressDetail(
@@ -114,7 +119,7 @@ function externalTaskProgressDetail(
}
const values = [
Number.isFinite(task.progress) && task.progress > 0
? `总进度 ${Math.min(100, Math.max(0, Math.round(task.progress)))}%`
? `${Math.min(100, Math.max(0, Math.round(task.progress)))}%`
: null,
task.phaseDetail.trim(),
].filter((value): value is string => Boolean(value));
@@ -445,23 +450,7 @@ export function ImageCanvasTaskSidebarView({
}, [externalTasks]);
const items = useMemo<TaskSidebarItem[]>(() => {
const externalVisibleTasks = mergeExternalTasks(
externalTasks.filter(
(task) => task.status === 'queued' || task.status === 'running',
),
externalTasks
.filter(isTerminalExternalTask)
.sort(
(left, right) =>
(parseTaskTimeMs(right.completedAt) ??
parseTaskTimeMs(right.updatedAt) ??
0) -
(parseTaskTimeMs(left.completedAt) ??
parseTaskTimeMs(left.updatedAt) ??
0),
)
.slice(0, completedListLimit),
);
const externalVisibleTasks = externalTasks;
const externalItems = externalVisibleTasks.map<TaskSidebarItem>((task) => {
const status = canvasTaskStatusFromExternalTask(task.status);
const startedAt =
@@ -493,7 +482,7 @@ export function ImageCanvasTaskSidebarView({
};
});
return externalItems;
}, [completedListLimit, externalTasks, now, onFocusExternalTask]);
}, [externalTasks, now, onFocusExternalTask]);
const activeItems = useMemo(
() =>
@@ -635,6 +624,7 @@ export function ImageCanvasTaskSidebarView({
const titleText = [detailText, item.progressDetail, timeText]
.filter((value): value is string => Boolean(value))
.join(' · ');
const metaTimeText = timeText;
return (
<button
key={item.id}
@@ -651,8 +641,15 @@ export function ImageCanvasTaskSidebarView({
<span className="image-canvas-editor__task-sidebar-item-title">
{item.title}
</span>
<span className="image-canvas-editor__task-sidebar-item-status">
{statusLabel(item.status)}
<span className="image-canvas-editor__task-sidebar-item-meta">
<span className="image-canvas-editor__task-sidebar-item-status">
{statusLabel(item.status)}
</span>
{metaTimeText ? (
<span className="image-canvas-editor__task-sidebar-item-time">
{metaTimeText}
</span>
) : null}
</span>
</span>
<span
@@ -669,11 +666,6 @@ export function ImageCanvasTaskSidebarView({
{item.progressDetail}
</span>
) : null}
{timeText ? (
<span className="image-canvas-editor__task-sidebar-item-time">
{timeText}
</span>
) : null}
</span>
</button>
);
+9 -1
View File
@@ -6445,7 +6445,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
min-width: 0;
align-items: center;
align-items: start;
gap: 0.45rem;
}
@@ -6485,6 +6485,14 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
white-space: nowrap;
}
.image-canvas-editor__task-sidebar-item-meta {
display: inline-flex;
min-width: max-content;
align-items: center;
justify-content: flex-end;
gap: 0.36rem;
}
.image-canvas-editor__task-sidebar-item-status {
color: #64748b;
font-size: 0.68rem;