Improve Objective list and detail display

This commit is contained in:
2026-09-17 22:51:07 +09:00
parent 96592b1a24
commit 5ae1bab4ee
6 changed files with 144 additions and 53 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview"
},
@@ -84,8 +84,7 @@
gap: var(--space-2);
min-width: 0;
}
.ticket-title,
.objective-title {
.ticket-title {
min-width: 0;
overflow: hidden;
color: var(--text-strong);
@@ -93,6 +92,14 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.objective-title {
min-width: 0;
flex: 1 1 auto;
color: var(--text-strong);
font-weight: 700;
overflow-wrap: anywhere;
white-space: normal;
}
.state-pill {
flex: 0 0 auto;
color: var(--success);
@@ -101,8 +108,7 @@
letter-spacing: 0.05em;
text-transform: uppercase;
}
.ticket-summary,
.objective-summary {
.ticket-summary {
margin: 0;
color: var(--text-muted);
font-size: 0.88rem;
@@ -133,6 +139,51 @@
white-space: normal;
}
}
.objective-title-row {
align-items: flex-start;
}
.objective-title-row.detail h3 {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}
.objective-detail-card {
display: grid;
gap: var(--space-4);
}
.objective-detail-card > h2 {
margin: 0;
}
.objective-back-link {
width: fit-content;
color: var(--text-muted);
font-size: 0.85rem;
}
.objective-detail-metadata {
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--line);
}
.objective-detail-metadata dd {
overflow-wrap: anywhere;
}
.objective-body {
min-width: 0;
}
.objective-body > :first-child {
margin-top: 0;
}
.objective-body > :last-child {
margin-bottom: 0;
}
@media (max-width: 600px) {
.objective-title-row.detail {
flex-wrap: wrap;
}
.objective-detail-metadata > div {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-1);
}
}
.runtime-heading,
.detail-heading {
display: flex;
@@ -25,7 +25,6 @@
<strong class="objective-title">{objective.title}</strong>
<span class="state-pill">{objective.state}</span>
</div>
<p class="objective-summary">{objective.summary || 'No summary text is available.'}</p>
</div>
<div class="objective-meta" aria-label="Objective metadata">
<span>Updated {objective.updated_at ? formatDate(objective.updated_at) : 'unknown'}</span>
@@ -1,52 +1,28 @@
<script lang="ts">
import RichMarkdown from '$lib/workspace/console/RichMarkdown.svelte';
import { formatDate } from '$lib/workspace/api/http';
import { objectiveHref, ticketHref } from '$lib/workspace/resource-links';
import { ticketHref } from '$lib/workspace/resource-links';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const objectivesHref = (workspaceId: string) =>
`/w/${encodeURIComponent(workspaceId)}/objectives`;
</script>
<svelte:head>
<title>{data.objective?.title ?? data.objectiveId} · Objective</title>
</svelte:head>
<section class="card">
<h2>Objectives</h2>
{#if data.objectives}
<div class="objective-list compact">
{#each data.objectives.items as objective (objective.id)}
<a class="objective-row" class:active={objective.id === data.objectiveId} href={objectiveHref(data.workspaceId, objective)}>
<div class="objective-main">
<div class="objective-title-row">
<strong class="objective-title">{objective.title}</strong>
<span class="state-pill">{objective.state}</span>
</div>
<p class="objective-summary">{objective.summary || 'No summary text is available.'}</p>
</div>
<div class="objective-meta" aria-label="Objective metadata">
<span>Updated {objective.updated_at ? formatDate(objective.updated_at) : 'unknown'}</span>
<code>{objective.resource_key}</code>
</div>
</a>
{/each}
</div>
{:else if data.objectivesError}
<p class="error">{data.objectivesError}</p>
{:else}
<p>Loading objectives…</p>
{/if}
</section>
<section class="card objective-detail-card">
<a class="objective-back-link" href={objectivesHref(data.workspaceId)}>← All Objectives</a>
<h2>Objective detail</h2>
{#if data.objective}
<div class="objective-title-row detail">
<div>
<h3>{data.objective.title}</h3>
</div>
<h3>{data.objective.title}</h3>
<span class="state-pill">{data.objective.state}</span>
</div>
<dl>
<dl class="objective-detail-metadata">
<div>
<dt>Created</dt>
<dd>{data.objective.created_at ? formatDate(data.objective.created_at) : 'unknown'}</dd>
@@ -72,7 +48,13 @@
</dd>
</div>
</dl>
<pre class="objective-body">{data.objective.body}</pre>
<div class="objective-body" aria-label="Objective body">
{#if data.objective.body}
<RichMarkdown text={data.objective.body} />
{:else}
<p class="section-note">No Objective body is available.</p>
{/if}
</div>
{#if data.objective.body_truncated}
<p class="error">Objective body was truncated by the Backend response limit.</p>
{/if}
@@ -4,22 +4,16 @@ import {
canonicalResourceReference,
resourceKey,
} from "$lib/workspace/resource-links";
import type {
ObjectiveDetail,
ObjectiveListResponse,
} from "$lib/workspace/sidebar/types";
import type { ObjectiveDetail } from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
const objectiveId = resourceKey(params.objectiveId);
const [objectives, objective] = await Promise.all([
loadJson<ObjectiveListResponse>(fetch, apiPath("/objectives")),
loadJson<ObjectiveDetail>(
fetch,
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
),
]);
const objective = await loadJson<ObjectiveDetail>(
fetch,
apiPath(`/objectives/${encodeURIComponent(objectiveId)}`),
);
if (objective.data) {
const canonical = canonicalResourceReference(
@@ -29,7 +23,9 @@ export const load: PageLoad = async ({ fetch, params }) => {
if (params.objectiveId !== canonical) {
redirect(
308,
`/w/${encodeURIComponent(params.workspaceId)}/objectives/${encodeURIComponent(canonical)}`,
`/w/${encodeURIComponent(params.workspaceId)}/objectives/${
encodeURIComponent(canonical)
}`,
);
}
}
@@ -37,8 +33,6 @@ export const load: PageLoad = async ({ fetch, params }) => {
return {
workspaceId: params.workspaceId,
objectiveId,
objectives: objectives.data,
objectivesError: objectives.error,
objective: objective.data,
objectiveError: objective.error,
};
@@ -0,0 +1,65 @@
import {
assert,
assertFalse,
assertMatch,
assertStringIncludes,
} from "jsr:@std/assert";
const objectivesListSource = await Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/objectives/+page.svelte",
import.meta.url,
),
);
const objectiveDetailSource = await Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.svelte",
import.meta.url,
),
);
const objectiveDetailLoaderSource = await Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts",
import.meta.url,
),
);
const workspaceStyles = await Deno.readTextFile(
new URL("../src/lib/workspace/styles/workspace-pages.css", import.meta.url),
);
Deno.test("Objective list shows complete titles without body summaries", () => {
assertStringIncludes(objectivesListSource, "{objective.title}");
assertFalse(objectivesListSource.includes("objective.summary"));
assertMatch(
workspaceStyles,
/\.objective-title\s*\{[^}]*overflow-wrap:\s*anywhere;[^}]*white-space:\s*normal;/s,
);
});
Deno.test("Objective detail focuses on one Markdown-rendered Objective", () => {
assertStringIncludes(
objectiveDetailSource,
"<RichMarkdown text={data.objective.body} />",
);
assertFalse(objectiveDetailSource.includes('class="objective-list'));
assertFalse(objectiveDetailSource.includes("data.objectives"));
assertStringIncludes(objectiveDetailSource, "linked_ticket_summaries");
assertStringIncludes(objectiveDetailSource, "body_truncated");
});
Deno.test("Objective detail loader fetches only the selected Objective", () => {
assertStringIncludes(
objectiveDetailLoaderSource,
"`/objectives/${encodeURIComponent(objectiveId)}`",
);
assertFalse(
objectiveDetailLoaderSource.includes('apiPath("/objectives")'),
);
assertFalse(
objectiveDetailLoaderSource.includes("ObjectiveListResponse"),
);
assert(
objectiveDetailLoaderSource.includes("canonicalResourceReference"),
"canonical Objective redirects must remain intact",
);
});