Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/deviations-from-angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ One deviation runs through most of the `api:` entries below: **DEMI is the only
- pages/content-search: a passage carries no author. The Angular card showed a document's type, milestone and date; a chunk row carries no author field, so the meta line under the file name is date and type only.
- pages/project: the document-type tabs (Application, Certificate, Amendment(s)) are sub-tabs of Documents, joined by new C&E Documents and Management Plan sub-tabs. Old top-level paths redirect to the sub-tab, query string intact.
- pages/project: the project page has seven sections (Overview, Timeline, Updates, Engagement, Documents, Decisions, Compliance) as a `<nav>` of links rather than Angular's three ARIA tabs plus scattered document tabs. Timeline shows on every project, but not on a project notification or on an extended page whose tabs leave it out (Pacific Link). It replaces the rail's Detailed view: the Overview rail keeps the simple strip and links to the tab. Documents shows a count from a pageSize=1 search. Updates reads the project's whole Update list (`/demi/api/updates?project=<Eagle id>&sort=-publishDate`, 100 rows a page until `x-total-count`) on every project page, because only published rows count and a project with none hides the tab; the Overview panel and the Updates tab read the same cached list, which replaces Angular's `pageSize=10&sortBy=-dateAdded` read. Decisions and Compliance appear only when the project has a decision or the matching documents, so the pageSize=1 presence probes fire on every project page again (the same ones the Documents sub-tabs use; one query key, no duplicate fetch). The request baseline drops those probe lines; `documents-page.spec.tsx` and `project.spec.tsx` cover them. `/project-details` redirects to `/overview`, `/commenting` to `/engagement`.
- pages/project: the Timeline dates every Act the project has from its documents, where Track dates only the 2018 Act, by design. A phase runs from its first to its last document posting (`projectPhase`, `datePosted`), read from DEMI. A stage Track dates keeps Track's dates, a phase only documents name is added, and Eagle's `phaseHistory` names the rest without dates. A document or comment period belongs to the latest Act the project had moved to by its date: from the recorded transition date, else from when that Act came into force.
- pages/project: two guards keep a mis-dated document from stretching a phase. A document dated before its Act came into force is dropped. So is a leading run of under a tenth of a phase's documents that sits over two years before the rest.
- pages/project: a comment period draws on the Timeline as a milestone with a start and an end. The documents come from `/documents?project=<Track id>&page=N&pageSize=100&count=true&sort=datePosted` with no filter, up to DEMI's 2000-row anonymous limit. When that limit cuts the read short, an empty step reads "None among the documents read" and "View all" drops its count.
- pages/project: the Timeline details panel lists a step's documents, newest first, 50 inline. A phase lists the documents filed under it by List id and a comment period its related documents. A second assessment's phases list none, because the tags cannot tell the two assessments apart. A decision lists the documents its certificate type, milestone or name marks, else the decision phase's.
- pages/project: past 50 documents, the Timeline panel's "View all N documents" opens the Documents page filtered by `projectPhase`, and drops its count when another row lists documents under the same tags; a decision opens the certificate sub-tab and a comment period its own page. Each link is the site's `DocumentLink`, opening in a new tab with PDFs inline.
- pages/project: on the Timeline lanes, the view box on the mini-map is a slider. Drag it, or use the keys: Left and Down move it earlier, Right and Up later (Shift for larger steps), Page Down and Page Up by half a view, Home and End to either end. A second box on the lanes marks the list rows in view. Only the 24px strip on that box drags, and dragging it scrolls the list to the dates it covers.
- pages/project: the overview tab drops Angular's "Project ID" fact. No project carries `epicProjectID` on test or on prod, on either backend, so the row only ever rendered its dash. The model field stays: the project list filter still searches on it.
- state/map-ui: ConfigService's `mapBounds` getter dropped; nothing in the app ever read it.
- state/map-ui: `storage.service` dropped. Its background preload was never called from anywhere, and TanStack Query's cache already gives "fetch the project list once, reuse it on the way back".
Expand Down
52 changes: 50 additions & 2 deletions e2e/tests/project-timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ test.describe('desktop', () => {
});

test('?item= preselects that row and opens its folded group', async ({ page }) => {
await openTimeline(page, '?item=phase-2002-1');
// Pre-EA is dated by its documents; Pre-Application is the one undated 2002 phase.
await openTimeline(page, '?item=phase-2002-pre-application');

await expect(row(page, 'history-2002')).toHaveAttribute('aria-expanded', 'true');
await expect(row(page, 'phase-2002-1')).toHaveAttribute('aria-pressed', 'true');
await expect(row(page, 'phase-2002-pre-application')).toHaveAttribute('aria-pressed', 'true');
await expect(
page.getByRole('complementary', { name: 'Selected item' }).getByRole('heading', { level: 3 }),
).toHaveText('Pre-Application');
Expand All @@ -95,6 +96,53 @@ test.describe('desktop', () => {
await list.evaluate((element) => element.scrollTo({ top: element.scrollHeight }));
await expect(heading).toHaveText(new RegExp(`list showing (?!2002)\\d{4} – ${thisYear}$`));
});

test('dragging the strip of the list box on the lanes scrolls the list', async ({ page }) => {
await openTimeline(page);
const list = page.locator('.timeline-list');
await list.evaluate((element) => element.scrollTo({ top: 0 }));
const handle = page.locator('.timeline-lanes__window-handle');
await handle.scrollIntoViewIfNeeded();
await expect(list).toHaveJSProperty('scrollTop', 0);
const box = (await handle.boundingBox())!;
const y = box.y + box.height / 2;

await page.mouse.move(box.x + box.width / 2, y);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 300, y, { steps: 10 });
await page.mouse.up();

await expect.poll(() => list.evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
});

test('dragging the view box on the mini-map moves the zoomed view', async ({ page }) => {
await openTimeline(page);
await page.getByRole('button', { name: 'Zoom in' }).click();
const slider = page.getByRole('slider', { name: 'View on the whole life' });
await expect(slider).toHaveAttribute('aria-valuenow', '50');
const box = (await slider.boundingBox())!;
const y = box.y + box.height / 2;

await page.mouse.move(box.x + box.width / 2, y);
await page.mouse.down();
// Half the mini-map is the whole way left at this zoom; the view stops at the start.
await page.mouse.move(box.x + box.width / 2 - box.width, y, { steps: 10 });
await page.mouse.up();

await expect(slider).toHaveAttribute('aria-valuenow', '0');
});

test('a phase with over 50 documents links to all of them by phase tag', async ({ page }) => {
await openTimeline(page, '?item=phase-6');

const all = page
.getByRole('complementary', { name: 'Selected item' })
.getByRole('link', { name: /^View all \d+ documents/ });
await expect(all).toHaveAttribute(
'href',
new RegExp(`^/p/${CARIBOO}/documents\\?projectPhase=[0-9a-f]{24}(%2C[0-9a-f]{24})*$`),
);
});
});

test.describe('phone', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/app/components/table/document-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import {
export function DocumentLink({
document,
children,
className,
onClick,
}: {
document: DownloadableDocument;
children: ReactNode;
className?: string;
/** Runs before the download starts, for a caller that reports where the click came from. */
onClick?: () => void;
}) {
const inline = isInlineViewable(document);
return (
<a
className={className}
href={documentLinkUrl(document)}
target="_blank"
rel="noopener noreferrer"
Expand Down
20 changes: 19 additions & 1 deletion src/app/pages/project/timeline-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,19 @@ export function legendOf(
model.items.flatMap((item) => (item.kind === 'work' ? [item.status] : [])),
);
const has = (kind: TimelineItem['kind']) => model.items.some((item) => item.kind === kind);
// The lanes draw a milestone with an end after its start, a comment period, as a span.
const spans = (item: TimelineItem) =>
item.kind === 'milestone' && (time(item.end) ?? -Infinity) > (time(item.start) ?? Infinity);
return [
model.items.some((item) => hatchRange(item)) && ['undated', 'Not dated'],
plannedDecisions && ['planned', 'Planned, to the planned decision'],
lanes && plannedDecisions && ['planned-decision', 'Planned decision'],
lanes && has('milestone') && ['milestone', 'Milestone'],
lanes &&
model.items.some((item) => item.kind === 'milestone' && !spans(item)) && [
'milestone',
'Milestone',
],
lanes && model.items.some(spans) && ['milestone-span', 'Comment period'],
model.transitions.some((one) => one.between?.from && one.between.to) && [
'transition',
'Transition happened in here',
Expand All @@ -254,3 +262,13 @@ export function legendOf(
['today', 'Today'],
].filter((entry): entry is [string, string] => !!entry);
}

/** The timeline list under `host`. */
export function listElement(host: HTMLElement | null): HTMLElement | null {
return host?.querySelector<HTMLElement>('.timeline-list') ?? null;
}

/** The rows of `list`; each carries its item id as `data-id`. */
export function listRows(list: HTMLElement | null): HTMLLIElement[] {
return list ? [...list.querySelectorAll<HTMLLIElement>(':scope > li[data-id]')] : [];
}
24 changes: 24 additions & 0 deletions src/app/pages/project/timeline-details.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@
border-top: var(--layout-border-width-small) solid var(--theme-gray-30);
}

/* When the panel has a height cap, only the document list gives up room, and scrolls. */
.timeline-details > * {
flex-shrink: 0;
}

.timeline-details__section--documents {
flex-shrink: 1;
min-height: 0;
}

.timeline-details__section--documents > .timeline-details__documents {
flex: 0 1 auto;
min-height: 0;
}

.timeline-details__related {
display: flex;
flex-direction: column;
Expand All @@ -116,6 +131,7 @@
justify-content: space-between;
gap: var(--layout-padding-small);
min-height: 32px;
flex-shrink: 0;
}

.timeline-details__link {
Expand All @@ -129,6 +145,13 @@
cursor: pointer;
}

.timeline-details__documents a {
min-width: 0;
font: var(--typography-regular-small-body);
color: var(--typography-color-link);
overflow-wrap: anywhere;
}

.timeline-details__aside {
font: var(--typography-regular-label);
color: var(--typography-color-secondary);
Expand All @@ -148,6 +171,7 @@
}

.timeline-details__link:focus-visible,
.timeline-details__documents a:focus-visible,
.whose-time__bar:focus-visible {
outline: 2px solid var(--surface-color-border-active);
outline-offset: 2px;
Expand Down
Loading
Loading