All files / src versions-webview-html.ts

100% Statements 160/160
69.23% Branches 9/13
100% Functions 8/8
100% Lines 160/160

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 1611x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import * as crypto from "node:crypto"
 
import { planningPayloadForVersionsView, type PlanningPayload } from "./planning-payload.js"
import {
  FILTER_CHIP_SCRIPT,
  PAGINATION_SCRIPT,
  PLANNING_DETAIL_SCRIPT,
  PLANNING_WEBVIEW_STYLES,
  planningWebviewShell,
} from "./webview-common.js"
import {
  PROJECT_CONTEXT_SCRIPT,
  PROJECT_CONTEXT_STYLES,
  projectContextToolbarHtml,
  type WebviewProjectContext,
} from "./webview-project-context.js"
 
export function versionsWebviewHtml(
  payload: PlanningPayload,
  context: WebviewProjectContext,
): string {
  const nonce = crypto.randomBytes(16).toString("hex")
  const sorted = planningPayloadForVersionsView(payload)
  const dataJson = JSON.stringify(sorted)
  const contextJson = JSON.stringify(context)
  const body = `
  <div class="toolbar">
    ${projectContextToolbarHtml(context)}
  </div>
  <div class="main" id="list"></div>
  <div class="pager" id="pager"></div>`
  const script = `
    const vscode = acquireVsCodeApi();
    const payload = ${dataJson};
    const projectContext = ${contextJson};
    ${PROJECT_CONTEXT_SCRIPT}
    wireProjectContext(projectContext);
    ${FILTER_CHIP_SCRIPT}
    ${PAGINATION_SCRIPT}
    ${PLANNING_DETAIL_SCRIPT}
    const STATE_VERSION = 6;
    const saved = vscode.getState() || {};
    let expanded = new Set(
      saved.stateVersion === STATE_VERSION && saved.expanded
        ? saved.expanded
        : payload.versions.slice(0, 1).map((v) => v.id),
    );
    let pageSize = saved.stateVersion === STATE_VERSION
      ? normalizePageSize(saved.pageSize)
      : DEFAULT_PAGE_SIZE;
    let currentPage = saved.stateVersion === STATE_VERSION && saved.currentPage
      ? saved.currentPage
      : 1;
 
    function persist() {
      vscode.setState({
        stateVersion: STATE_VERSION,
        expanded: [...expanded],
        pageSize,
        currentPage,
      });
    }
 
    function epicsInVersion(versionId, scoped) {
      const ids = new Set(scoped.map((s) => s.epic));
      return payload.epics.filter((e) => ids.has(e.id));
    }
 
    function renderList() {
      const root = document.getElementById("list");
      const all = payload.versions;
      const paged = pageSlice(all, currentPage, pageSize);
      currentPage = paged.page;
      root.innerHTML = "";
      for (const version of paged.slice) {
        const open = expanded.has(version.id);
        const scoped = payload.stories.filter((s) => s.version === version.id);
        const p = progress(scoped);
        const versionSprints = payload.sprints.filter((s) => s.version === version.id);
        const block = document.createElement("div");
        block.className = "block" + (open ? " open" : "");
 
        const head = document.createElement("div");
        head.className = "block-head";
        const toggle = () => {
          if (expanded.has(version.id)) expanded.delete(version.id);
          else expanded.add(version.id);
          persist();
          renderList();
        };
        head.append(
          makeAccordionHead(open, toggle),
          makeIdLink(version.id, () => vscode.postMessage({ type: "openVersion", id: version.id })),
        );
        const title = document.createElement("span");
        title.className = "row-title";
        title.textContent = version.title;
        head.append(title, makeBadge(version.status), makeHeadProgress(scoped));
        block.appendChild(head);
 
        if (open) {
          const bodyEl = document.createElement("div");
          bodyEl.className = "block-body";
          appendDetailLine(bodyEl, "Outcome:", version.outcome);
          appendProgressBar(bodyEl, p.done, p.total);
 
          if (versionSprints.length) {
            const sprintSection = appendSection(bodyEl, "Sprints");
            for (const sp of versionSprints) {
              const spStories = storiesForIds(sp.storyIds, payload.stories);
              const spProgress = progress(spStories);
              appendMiniLink(
                sprintSection,
                sp.id,
                sp.status + " · " + spProgress.done + "/" + spProgress.total + " stories · " + truncate(sp.title, 80),
                () => vscode.postMessage({ type: "openSprint", id: sp.id }),
              );
            }
          }
 
          const versionEpics = epicsInVersion(version.id, scoped);
          if (versionEpics.length) {
            const epicSection = appendSection(bodyEl, "Epics with stories in this version");
            for (const epic of versionEpics) {
              const epicStories = scoped.filter((s) => s.epic === epic.id);
              const ep = progress(epicStories);
              appendMiniLink(
                epicSection,
                epic.id,
                epic.status + " · " + ep.done + "/" + ep.total + " · " + truncate(epic.title, 72),
                () => vscode.postMessage({ type: "openEpic", id: epic.id }),
              );
            }
          }
 
          const hint = document.createElement("p");
          hint.className = "meta-line";
          hint.textContent =
            versionSprints.length +
            " sprint(s) · " +
            versionEpics.length +
            " epic(s) — open version file for release notes and go-live checklist.";
          bodyEl.appendChild(hint);
          block.appendChild(bodyEl);
        }
        root.appendChild(block);
      }
      currentPage = renderPager(
        document.getElementById("pager"),
        all.length,
        currentPage,
        pageSize,
        (p) => { currentPage = p; persist(); renderList(); },
        (size) => { pageSize = size; currentPage = 1; persist(); renderList(); },
      );
    }
    renderList();
  `
  return planningWebviewShell(nonce, PLANNING_WEBVIEW_STYLES + PROJECT_CONTEXT_STYLES, body, script)
}