chore: retain committed Ticket sidebar changes during notification integration
This commit is contained in:
@@ -408,14 +408,20 @@ async function assertThreadLayout(page: Page) {
|
||||
assertEquals(await page.locator(".ticket-delivery").count(), 0);
|
||||
assertEquals(await page.getByText("Delivery and execution", { exact: true }).count(), 0);
|
||||
assertEquals(await rail.locator(".ticket-thread-event[data-initial=true], .ticket-thread-event, .ticket-thread-composer").count(), 0);
|
||||
for (const selector of [".ticket-orchestrator-worker", ".ticket-worker-observation", ".ticket-worker-card", ".ticket-resources"]) {
|
||||
for (const selector of [".ticket-orchestrator-worker", ".ticket-worker-observation", ".ticket-worker-card", ".ticket-resource-editor"]) {
|
||||
const details = rail.locator(selector);
|
||||
assertEquals(await details.evaluate((element) => element.tagName), "DETAILS");
|
||||
assertEquals(await details.getAttribute("open"), null);
|
||||
assert(await details.locator(":scope > summary").isVisible());
|
||||
}
|
||||
assertEquals(await rail.locator(".ticket-worker-card > summary").textContent(), "Role assignments and manual Workers");
|
||||
assert(/^Repository resources \(\d+\)$/.test((await rail.locator(".ticket-resources > summary").textContent())!));
|
||||
assert(await rail.getByRole("heading", { name: "Repository resources", exact: true }).isVisible());
|
||||
assert(await rail.locator(".ticket-resource-summary").isVisible());
|
||||
assertEquals(await rail.locator(":scope > section").first().getAttribute("aria-label"), "Repository resources");
|
||||
assert(await rail.locator(".ticket-resources").evaluate((element) =>
|
||||
element.getBoundingClientRect().bottom <= element.parentElement!.querySelector(".ticket-handling")!.getBoundingClientRect().top
|
||||
));
|
||||
assertEquals(await rail.locator(".ticket-resource-editor > summary").textContent(), "Edit resources");
|
||||
const evidence = rail.locator("details").filter({ has: page.locator("summary", { hasText: /^MR requirement evidence$/ }) });
|
||||
if (await evidence.count()) assertEquals(await evidence.getAttribute("open"), null);
|
||||
}
|
||||
@@ -1090,6 +1096,43 @@ Deno.test("canonical thread renders long initial post and chronological multilin
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Repository summary stays visible while edits remain explicit and refresh after saving", async () => {
|
||||
await withFixture(async (browser, base) => {
|
||||
const page = await browser.newPage();
|
||||
const wire = await detail(base, "T-1");
|
||||
wire.targets = [{ repository_key: "main", ref_selector: null, access: "read_only" }];
|
||||
const writes: unknown[] = [];
|
||||
await page.route("**/api/w/tickets/tickets/*", async (route) => {
|
||||
const reference = decodeURIComponent(new URL(route.request().url()).pathname.split("/").at(-1)!);
|
||||
if (reference !== "T-1" && reference !== wire.id) return await route.continue();
|
||||
if (route.request().method() === "PATCH") {
|
||||
const body = route.request().postDataJSON();
|
||||
writes.push(body);
|
||||
wire.targets = body.target.action === "clear" ? [] : body.target.targets;
|
||||
}
|
||||
await route.fulfill({ status: 200, json: wire });
|
||||
});
|
||||
await page.goto(`${base}/w/tickets/tickets/T-1`);
|
||||
const resources = page.getByRole("region", { name: "Repository resources", exact: true });
|
||||
await resources.getByText("Ref not specified", { exact: true }).waitFor();
|
||||
const summary = resources.locator(".ticket-resource-editor > summary");
|
||||
await summary.focus();
|
||||
await summary.press("Enter");
|
||||
await resources.getByLabel("Ref selector", { exact: true }).fill("feature/sidebar");
|
||||
await resources.getByRole("combobox", { name: /^Access/ }).selectOption("read_write");
|
||||
assertEquals(writes, []);
|
||||
assertEquals(await resources.locator(".ticket-resource-summary code").textContent(), "Ref not specified");
|
||||
await resources.getByRole("button", { name: "Save targets", exact: true }).click();
|
||||
await resources.locator(".ticket-resource-summary code").filter({ hasText: "feature/sidebar" }).waitFor();
|
||||
assertEquals(writes, [{ target: { action: "set", targets: [{ repository_key: "main", ref_selector: "feature/sidebar", access: "read_write" }] } }]);
|
||||
assertEquals(await resources.locator(".ticket-resource-access").textContent(), "Read/write");
|
||||
await resources.getByRole("button", { name: "Remove target", exact: true }).click();
|
||||
await resources.getByRole("button", { name: "Save targets", exact: true }).click();
|
||||
await resources.locator(".ticket-resource-summary").getByText("No repository targets.", { exact: true }).waitFor();
|
||||
assertEquals(writes[1], { target: { action: "clear" } });
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Ticket metadata stays in the rail with flat links, wrapping labels and keyboard navigation", async () => {
|
||||
await withFixture(async (browser, base) => {
|
||||
const output = artifactPath("metadata");
|
||||
@@ -1102,6 +1145,10 @@ Deno.test("Ticket metadata stays in the rail with flat links, wrapping labels an
|
||||
const body = await response.json();
|
||||
if (body.resource_key === "T-1" && body.relations) {
|
||||
body.tags[0].name = "Classification-with-a-long-name-".repeat(4);
|
||||
body.targets = [
|
||||
{ repository_key: "Repository-with-a-long-name-".repeat(4), ref_selector: "feature/long-selector-".repeat(6), access: "read_write" },
|
||||
{ repository_key: "docs", ref_selector: null, access: "read_only" },
|
||||
];
|
||||
body.relations.outgoing = [{ at: "2026-01-01T00:00:00Z", author: "fixture-owner", kind: "related", target: "internal-T-2", target_resource_key: "T-2", ticket_id: body.id, note: "Related investigation with a long note. ".repeat(5) }];
|
||||
}
|
||||
await route.fulfill({ response, json: body });
|
||||
@@ -1111,6 +1158,13 @@ Deno.test("Ticket metadata stays in the rail with flat links, wrapping labels an
|
||||
const link = rail.getByRole("link", { name: /T-2/ });
|
||||
await link.waitFor();
|
||||
assertEquals(await link.getAttribute("href"), "/w/tickets/tickets/T-2");
|
||||
const resources = rail.getByRole("region", { name: "Repository resources", exact: true });
|
||||
assertEquals(await resources.locator(".ticket-resource-summary strong").allTextContents(), ["Repository-with-a-long-name-".repeat(4), "docs"]);
|
||||
assertEquals(await resources.locator(".ticket-resource-summary code").allTextContents(), ["feature/long-selector-".repeat(6), "Ref not specified"]);
|
||||
assertEquals(await resources.locator(".ticket-resource-access").allTextContents(), ["Read/write", "Read only"]);
|
||||
assert(await resources.locator(".ticket-resource-summary").evaluate((element) => element.scrollWidth <= element.clientWidth));
|
||||
assertEquals(await resources.locator(".ticket-resource-editor").getAttribute("open"), null);
|
||||
await resources.screenshot({ path: `${output}/resources-${colorScheme}-${width}.png` });
|
||||
assertEquals(await page.locator(".ticket-detail-main .ticket-tags, .ticket-detail-main .ticket-relations").count(), 0);
|
||||
assertEquals(await page.evaluate("document.documentElement.scrollWidth > innerWidth"), false);
|
||||
assert(await rail.locator(".ticket-tag-chip, .ticket-relations-list a").evaluateAll((elements) => elements.every((element) => {
|
||||
|
||||
@@ -344,14 +344,24 @@
|
||||
}
|
||||
.ticket-post-attribution { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); min-width: 0; overflow-wrap: anywhere; }
|
||||
.ticket-post-edited { color: var(--text-muted); font-size: var(--font-size-compact); }
|
||||
.ticket-metadata dl { display: grid; gap: var(--space-1); margin: 0 0 var(--space-3); }
|
||||
.ticket-metadata dt { color: var(--text-muted); font-size: var(--font-size-compact); }
|
||||
.ticket-metadata dd { margin: 0 0 var(--space-2); overflow-wrap: anywhere; }
|
||||
.ticket-resource-summary { display: grid; gap: var(--space-2); list-style: none; padding: 0; margin: 0 0 var(--space-2); }
|
||||
.ticket-resource-summary li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-1) var(--space-2); align-items: baseline; }
|
||||
.ticket-resource-summary code { grid-column: 1 / -1; overflow-wrap: anywhere; color: var(--text-muted); font-size: inherit; }
|
||||
.ticket-resource-access { color: var(--text-muted); white-space: nowrap; }
|
||||
.ticket-metadata dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-3); margin: 0 0 var(--space-2); }
|
||||
.ticket-metadata dt { color: var(--text-muted); }
|
||||
.ticket-metadata dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.ticket-detail-page .ticket-control-rail { gap: var(--space-2); font-size: var(--font-size-compact); }
|
||||
.ticket-control-rail :is(.ticket-section-heading, .ticket-control-card > header) { margin-bottom: var(--space-2); }
|
||||
.ticket-control-rail :is(.ticket-section-heading h2, .ticket-control-card h2) { font-size: var(--font-size-compact); }
|
||||
.ticket-control-rail :is(.workspace-primary-button, .workspace-secondary-button, .workspace-danger-button) { min-height: 2rem; padding: var(--space-1) var(--space-2); }
|
||||
.ticket-control-rail .ticket-relations-list a { grid-template-columns: auto minmax(0, 1fr); gap: var(--space-1) var(--space-2); padding-block: var(--space-1); }
|
||||
.ticket-relations-list small { grid-column: 1 / -1; }
|
||||
.ticket-metadata .ticket-editor { margin-top: var(--space-3); }
|
||||
.ticket-detail-page .ticket-control-rail summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-strong);
|
||||
font-size: var(--font-size-body);
|
||||
font-size: var(--font-size-compact);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ticket-control-rail details[open] > summary { margin-bottom: var(--space-3); }
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
{#if ticket.state === "open" && current?.last_event_id && current.tracking && current.tracking.status !== "done"}
|
||||
<form class="ticket-control-form ticket-answer" onsubmit={submitAnswer}>
|
||||
<label>Answer / continue this handling<textarea bind:value={answer} rows="3" required disabled={busy || pending !== null}></textarea></label>
|
||||
<p>Continues assignment <code>{current.assignment_id}</code>; not a new delegation. A stale assignment is rejected.</p>
|
||||
<button class="workspace-secondary-button" disabled={busy || pending !== null || !answer.trim()}>Send answer to this assignment</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
</script>
|
||||
<section class="ticket-detail-section ticket-tags">
|
||||
<div class="ticket-section-heading"><h2>Classification tags</h2></div>
|
||||
<p class="workspace-empty-copy">Classification only. Tag changes do not close or reopen this case.</p>
|
||||
<div class="ticket-tag-chips">{#each ticket.tags as tag (tag.id)}<span class="ticket-tag-chip">{tag.name}</span>{:else}<span>No tags</span>{/each}</div>
|
||||
{#if loading}<p role="status">Loading tag catalog…</p>{/if}
|
||||
{#if error}<p class="workspace-callout is-error" role="alert">{error}</p>{/if}
|
||||
|
||||
@@ -524,23 +524,60 @@
|
||||
<div class="ticket-column-divider"><BevelLine direction="y" length="100%" decorative /></div>
|
||||
<aside class="ticket-control-rail" aria-label="Ticket details">
|
||||
<div class="ticket-mobile-sidebar-divider"><BevelLine decorative /></div>
|
||||
<section class="ticket-control-card ticket-metadata" aria-label="Ticket metadata">
|
||||
<dl>
|
||||
<dt>Created by</dt><dd>{ticket.created_by ?? "Unknown author"}</dd>
|
||||
<dt>Created</dt><dd><time datetime={ticket.created_at ?? undefined}>{prettyDate(ticket.created_at)}</time></dd>
|
||||
<dt>Updated</dt><dd><time datetime={ticket.updated_at ?? undefined}>{prettyDate(ticket.updated_at)}</time></dd>
|
||||
</dl>
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => { editTitle = ticket.title; editing = !editing; }}>
|
||||
{editing ? "Cancel title edit" : "Edit title"}
|
||||
</button>
|
||||
{#if editing}
|
||||
<form class="ticket-editor" aria-label="Edit ticket title" onsubmit={saveEdit}>
|
||||
<label>Title<input bind:value={editTitle} required disabled={busy !== null} /></label>
|
||||
<button class="workspace-primary-button" type="submit" disabled={busy !== null || !editTitle.trim()}>
|
||||
{busy === "edit" ? "Saving…" : "Save title"}
|
||||
<section class="ticket-control-card ticket-resources" aria-label="Repository resources">
|
||||
<div class="ticket-section-heading"><h2>Repository resources</h2><span>{ticket.targets.length}</span></div>
|
||||
<ul class="ticket-resource-summary">
|
||||
{#each ticket.targets as target}
|
||||
<li>
|
||||
<strong>{target.repository_key}</strong>
|
||||
<span class="ticket-resource-access">{target.access === "read_write" ? "Read/write" : "Read only"}</span>
|
||||
<code>{target.ref_selector ?? "Ref not specified"}</code>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="workspace-empty-copy">No repository targets.</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if data.repositories.error}<p class="workspace-callout is-error">Repository catalog unavailable: {data.repositories.error}</p>{/if}
|
||||
<details class="ticket-resource-editor">
|
||||
<summary>Edit resources</summary>
|
||||
<p class="workspace-empty-copy">Target changes do not reconnect Workers or change live permissions.</p>
|
||||
<form class="ticket-control-form" onsubmit={saveTarget}>
|
||||
<div class="ticket-target-list">
|
||||
{#each targetDrafts as target, index}
|
||||
{@const selectedRepository = repositoryFor(target.repository_key)}
|
||||
<fieldset class="ticket-target-row">
|
||||
<legend>Target {index + 1}</legend>
|
||||
<label>Repository
|
||||
<select bind:value={target.repository_key} disabled={busy !== null} required>
|
||||
<option value="">Choose repository</option>
|
||||
{#each loadedRepositories?.items ?? [] as repository}
|
||||
<option value={repository.repository_key}>{repository.repository_key}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or commit"} disabled={busy !== null} /></label>
|
||||
<label>Access
|
||||
<select bind:value={target.access} disabled={busy !== null}>
|
||||
<option value="read_write">Read and write</option>
|
||||
<option value="read_only">Read only</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => removeTarget(index)}>Remove target</button>
|
||||
</fieldset>
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">No repository targets.</p>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="workspace-secondary-button"
|
||||
type="button"
|
||||
onclick={() => addTarget()}
|
||||
>Add target</button>
|
||||
<button class="workspace-secondary-button" type="submit" disabled={busy !== null || !targetCandidateValid}>
|
||||
{busy === "target" ? "Saving…" : "Save targets"}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</details>
|
||||
</section>
|
||||
<BevelLine decorative />
|
||||
{#each ticket.action_eligibility.blockers as blocker}<p class="workspace-callout">{blocker}</p>{/each}
|
||||
@@ -602,6 +639,50 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if mergeRequests.length > 0}
|
||||
<BevelLine decorative />
|
||||
<details class="ticket-control-card">
|
||||
<summary>MR requirement evidence</summary>
|
||||
<p><strong>Current requirements:</strong> {ticket.evidence.complete_for_integration ? "satisfied for integration" : "not satisfied for integration"}.</p>
|
||||
<p><strong>Current requirement approval:</strong> {currentRequirementApprovalStatus(ticket.evidence)}.</p>
|
||||
{#if ticket.evidence.missing.length > 0}
|
||||
<ul>
|
||||
{#each ticket.evidence.missing as missing}<li>{missing}</li>{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
<p class="workspace-empty-copy">This evaluates linked MR requirements, separately from the Ticket case judgment and persisted integration approval.</p>
|
||||
{#if ticket.state === "closed"}
|
||||
<p class="workspace-empty-copy">This case remains closed. Incomplete MR evidence does not alter the recorded case judgment.</p>
|
||||
{/if}
|
||||
</details>
|
||||
{/if}
|
||||
<BevelLine decorative />
|
||||
<section class="ticket-control-card">
|
||||
<header><h2>Merge Requests</h2></header>
|
||||
{#if mergeRequests.length > 0}
|
||||
{#each mergeRequests as mergeRequest (mergeRequest.merge_request_id)}
|
||||
<article class="ticket-control-card">
|
||||
<p><strong>{mergeRequest.repository_key}: {mergeRequest.state}</strong></p>
|
||||
<p>
|
||||
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
|
||||
to <code>{mergeRequest.selector_to}</code>
|
||||
</p>
|
||||
<p><strong>{mergeRequest.state === "merged" ? "Immutable integration evidence" : "Live source review"}:</strong> {summarySourceReviewStatus(mergeRequest)}</p>
|
||||
{#if mergeRequest.integration_evidence_error !== null}
|
||||
<p class="workspace-callout is-error"><strong>Integration evidence error:</strong> {mergeRequest.integration_evidence_error}</p>
|
||||
{/if}
|
||||
<p><strong>Target integration:</strong> {mergeRequest.state === "merged" ? "recorded" : `awaiting Orchestrator integration into ${mergeRequest.selector_to}`}.</p>
|
||||
<a
|
||||
class="workspace-secondary-button"
|
||||
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}
|
||||
>Open Merge Request</a>
|
||||
</article>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">No Merge Requests linked.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<BevelLine decorative />
|
||||
{#key JSON.stringify([data.workspaceId, ticket.id])}
|
||||
<TicketWorkerObservation workspaceId={data.workspaceId} {ticket} />
|
||||
@@ -609,7 +690,7 @@
|
||||
<BevelLine decorative />
|
||||
<details class="ticket-control-card ticket-worker-card">
|
||||
<summary>Role assignments and manual Workers</summary>
|
||||
<p class="workspace-empty-copy">Responsibility is retained after work ends and Worker removal; it does not indicate a running Worker or active work authority.</p>
|
||||
<p class="workspace-empty-copy">Assignments are retained independently of Worker state.</p>
|
||||
{#if ticket.assignments.length > 0}
|
||||
<ul class="ticket-assignment-list">
|
||||
{#each ticket.assignments as assignment}
|
||||
@@ -633,11 +714,10 @@
|
||||
{#if implementationStartEligible}
|
||||
<a class="workspace-secondary-button" href={ticketWorkerLaunchHref(data.workspaceId, ticket)}>Start a Ticket Worker</a>
|
||||
<details><summary>Assign an existing Worker</summary>
|
||||
<p class="workspace-empty-copy">Explicit assignment is separate from state. Backend validates the Worker and resource binding; a retained assignment is not execution authority.</p>
|
||||
<form class="ticket-control-form" onsubmit={assignExistingWorker}>
|
||||
<label>Runtime ID<input bind:value={manualRuntimeId} required /></label>
|
||||
<label>Worker ID<input bind:value={manualWorkerId} required /></label>
|
||||
<p class="workspace-empty-copy">Select only resources needed for this request. No selection binds no Workdirs; unrelated existing attachments are not inherited. Use authorized connection IDs for explicit rebind.</p>
|
||||
<p class="workspace-empty-copy">Only selected Workdirs are bound. Rebinding requires authorized connection IDs.</p>
|
||||
{#each manualBindings as binding, index}
|
||||
<fieldset class="ticket-target-row"><legend>Resource binding {index + 1}</legend>
|
||||
<label>Alias<input bind:value={binding.alias} required /></label>
|
||||
@@ -682,95 +762,22 @@
|
||||
</details>
|
||||
|
||||
<BevelLine decorative />
|
||||
<details class="ticket-control-card ticket-resources">
|
||||
<summary>Repository resources ({ticket.targets.length})</summary>
|
||||
<p class="workspace-empty-copy">Targets describe permitted resources, not connected attachments. Editing targets or the body does not expand live permissions. Changed resources require explicit reconnection; stale or unavailable connections are rejected by Backend.</p>
|
||||
{#if data.repositories.error}<p class="workspace-callout is-error">Repository catalog unavailable: {data.repositories.error}</p>{/if}
|
||||
<form class="ticket-control-form" onsubmit={saveTarget}>
|
||||
<div class="ticket-target-list">
|
||||
{#each targetDrafts as target, index}
|
||||
{@const selectedRepository = repositoryFor(target.repository_key)}
|
||||
<fieldset class="ticket-target-row">
|
||||
<legend>Target {index + 1}</legend>
|
||||
<label>Repository
|
||||
<select bind:value={target.repository_key} disabled={busy !== null} required>
|
||||
<option value="">Choose repository</option>
|
||||
{#each loadedRepositories?.items ?? [] as repository}
|
||||
<option value={repository.repository_key}>{repository.repository_key}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or commit"} disabled={busy !== null} /></label>
|
||||
<label>Access
|
||||
<select bind:value={target.access} disabled={busy !== null}>
|
||||
<option value="read_write">Read and write</option>
|
||||
<option value="read_only">Read only</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => removeTarget(index)}>Remove target</button>
|
||||
</fieldset>
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">No repository targets.</p>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="workspace-secondary-button"
|
||||
type="button"
|
||||
onclick={() => addTarget()}
|
||||
>Add target</button>
|
||||
<button class="workspace-secondary-button" type="submit" disabled={busy !== null || !targetCandidateValid}>
|
||||
{busy === "target" ? "Saving…" : "Save targets"}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{#if mergeRequests.length > 0}
|
||||
<BevelLine decorative />
|
||||
<details class="ticket-control-card">
|
||||
<summary>MR requirement evidence</summary>
|
||||
<p><strong>Current requirements:</strong> {ticket.evidence.complete_for_integration ? "satisfied for integration" : "not satisfied for integration"}.</p>
|
||||
<p><strong>Current requirement approval:</strong> {currentRequirementApprovalStatus(ticket.evidence)}.</p>
|
||||
{#if ticket.evidence.missing.length > 0}
|
||||
<ul>
|
||||
{#each ticket.evidence.missing as missing}<li>{missing}</li>{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
<p class="workspace-empty-copy">This evaluates linked MR requirements, separately from the Ticket case judgment and persisted integration approval.</p>
|
||||
{#if ticket.state === "closed"}
|
||||
<p class="workspace-empty-copy">This case remains closed. Incomplete MR evidence does not alter the recorded case judgment.</p>
|
||||
{/if}
|
||||
</details>
|
||||
{/if}
|
||||
<BevelLine decorative />
|
||||
<section class="ticket-control-card">
|
||||
<header><h2>Merge Requests</h2></header>
|
||||
<p class="workspace-empty-copy">Ticket closure is not MR approval or integration.</p>
|
||||
{#if mergeRequests.length > 0}
|
||||
{#each mergeRequests as mergeRequest (mergeRequest.merge_request_id)}
|
||||
<article class="ticket-control-card">
|
||||
<p><strong>{mergeRequest.repository_key}: {mergeRequest.state}</strong></p>
|
||||
<p>
|
||||
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
|
||||
to <code>{mergeRequest.selector_to}</code>
|
||||
</p>
|
||||
<p><strong>{mergeRequest.state === "merged" ? "Immutable integration evidence" : "Live source review"}:</strong> {summarySourceReviewStatus(mergeRequest)}</p>
|
||||
{#if mergeRequest.integration_evidence_error !== null}
|
||||
<p class="workspace-callout is-error"><strong>Integration evidence error:</strong> {mergeRequest.integration_evidence_error}</p>
|
||||
{/if}
|
||||
<p><strong>Target integration:</strong> {mergeRequest.state === "merged" ? "recorded" : `awaiting Orchestrator integration into ${mergeRequest.selector_to}`}.</p>
|
||||
{#if mergeRequest.state === "merged"}
|
||||
<p class="workspace-empty-copy">The merged source and integration approval are persisted evidence, not a live selector observation.</p>
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.</p>
|
||||
{/if}
|
||||
<a
|
||||
class="workspace-secondary-button"
|
||||
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}
|
||||
>Open Merge Request</a>
|
||||
</article>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">No Merge Requests linked. A Ticket can be closed with a comment or document result.</p>
|
||||
<section class="ticket-control-card ticket-metadata" aria-label="Ticket metadata">
|
||||
<dl>
|
||||
<dt>Created by</dt><dd>{ticket.created_by ?? "Unknown author"}</dd>
|
||||
<dt>Created</dt><dd><time datetime={ticket.created_at ?? undefined}>{prettyDate(ticket.created_at)}</time></dd>
|
||||
<dt>Updated</dt><dd><time datetime={ticket.updated_at ?? undefined}>{prettyDate(ticket.updated_at)}</time></dd>
|
||||
</dl>
|
||||
<button class="workspace-secondary-button" type="button" onclick={() => { editTitle = ticket.title; editing = !editing; }}>
|
||||
{editing ? "Cancel title edit" : "Edit title"}
|
||||
</button>
|
||||
{#if editing}
|
||||
<form class="ticket-editor" aria-label="Edit ticket title" onsubmit={saveEdit}>
|
||||
<label>Title<input bind:value={editTitle} required disabled={busy !== null} /></label>
|
||||
<button class="workspace-primary-button" type="submit" disabled={busy !== null || !editTitle.trim()}>
|
||||
{busy === "edit" ? "Saving…" : "Save title"}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -63,11 +63,7 @@ Deno.test("ticket detail fences stale mutation responses", () => {
|
||||
|
||||
Deno.test("ticket responsibility display does not claim Worker liveness or work authority", () => {
|
||||
assertStringIncludes(pageSource, "Role assignments and manual Workers");
|
||||
assertStringIncludes(pageSource, "after work ends and Worker removal");
|
||||
assertStringIncludes(
|
||||
pageSource,
|
||||
"does not indicate a running Worker or active work authority",
|
||||
);
|
||||
assertStringIncludes(pageSource, "Assignments are retained independently of Worker state.");
|
||||
assertStringIncludes(pageSource, "assignment.principal.runtime_id");
|
||||
assertStringIncludes(pageSource, "assignment.principal.worker_id");
|
||||
assert(!pageSource.includes("No active role assignment."));
|
||||
|
||||
Reference in New Issue
Block a user