From 5ae1bab4ee0ecf432e2df87894e0394011dcb0e6 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 17 Sep 2026 22:51:07 +0900 Subject: [PATCH] Improve Objective list and detail display --- web/workspace/deno.json | 2 +- .../lib/workspace/styles/workspace-pages.css | 59 +++++++++++++++-- .../w/[workspaceId]/objectives/+page.svelte | 1 - .../objectives/[objectiveId]/+page.svelte | 48 +++++--------- .../objectives/[objectiveId]/+page.ts | 22 +++---- web/workspace/test/objective-pages.test.ts | 65 +++++++++++++++++++ 6 files changed, 144 insertions(+), 53 deletions(-) create mode 100644 web/workspace/test/objective-pages.test.ts diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 0887fc85e..58759ffb6 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -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" }, diff --git a/web/workspace/src/lib/workspace/styles/workspace-pages.css b/web/workspace/src/lib/workspace/styles/workspace-pages.css index c599c7962..01ff534f8 100644 --- a/web/workspace/src/lib/workspace/styles/workspace-pages.css +++ b/web/workspace/src/lib/workspace/styles/workspace-pages.css @@ -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; diff --git a/web/workspace/src/routes/w/[workspaceId]/objectives/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/objectives/+page.svelte index 4b7a14f37..6eef837ea 100644 --- a/web/workspace/src/routes/w/[workspaceId]/objectives/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/objectives/+page.svelte @@ -25,7 +25,6 @@ {objective.title} {objective.state} -

{objective.summary || 'No summary text is available.'}

Updated {objective.updated_at ? formatDate(objective.updated_at) : 'unknown'} diff --git a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.svelte index dff8b622a..162928f51 100644 --- a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.svelte @@ -1,52 +1,28 @@ {data.objective?.title ?? data.objectiveId} · Objective -
-

Objectives

- {#if data.objectives} - - {:else if data.objectivesError} -

{data.objectivesError}

- {:else} -

Loading objectives…

- {/if} -
-
+ ← All Objectives

Objective detail

{#if data.objective}
-
-

{data.objective.title}

-
+

{data.objective.title}

{data.objective.state}
-
+ -
{data.objective.body}
+
+ {#if data.objective.body} + + {:else} +

No Objective body is available.

+ {/if} +
{#if data.objective.body_truncated}

Objective body was truncated by the Backend response limit.

{/if} diff --git a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts index bf4b1592f..e822b5a9a 100644 --- a/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/objectives/[objectiveId]/+page.ts @@ -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(fetch, apiPath("/objectives")), - loadJson( - fetch, - apiPath(`/objectives/${encodeURIComponent(objectiveId)}`), - ), - ]); + const objective = await loadJson( + 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, }; diff --git a/web/workspace/test/objective-pages.test.ts b/web/workspace/test/objective-pages.test.ts new file mode 100644 index 000000000..319b84f81 --- /dev/null +++ b/web/workspace/test/objective-pages.test.ts @@ -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, + "", + ); + 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", + ); +});