Improve Objective list and detail display
This commit is contained in:
@@ -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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user