All checks were successful
CI / Auth e2e pack (push) Successful in 4m37s
CI / Import/export fidelity gate (push) Successful in 43s
CI / Lint, typecheck, test (push) Successful in 2m54s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m14s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m8s
CD / Promote to Int (push) Successful in 9s
The read-only widget surface over page/pond data (ADR 0008 extension point `pageTool`): - Host: PageToolsPanel lists the pond's active pageTool surfaces behind disclosures — each sandbox iframe mounts lazily on first open and tears down on close. The same surfaces are insertable as plugin_block embeds (#76's insert picker now offers pageTool points too; the sandbox drives both through the same render lifecycle). - New `ui.scrollToHeading(headingId)` capability: outline ids are derived from the doc and never stamped into the DOM, so the host resolves the id to its heading position via the shared extractOutline and scrolls the matching rendered heading. - `readPond.listPages` now carries label *names* per summary (PagesService.pluginPageSummaries) — the page-index filter chips work on data the viewer could resolve anyway; per-page permission filtering stays in the service as before. - Reference plugins packages/plugins/toc and packages/plugins/page-index: real SDK consumers (createPlugin + windowTransport), bundled with esbuild into the package ZIP; i18n de/en is inlined at build time — the sandbox CSP forbids runtime fetches, the i18n/ files stay the single source. The toc re-fetches its outline on a slow poll, so live heading edits appear once the collab server has re-derived the content cache. - e2e page-tools.spec.ts covers the acceptance criteria: live outline updates after the persistence debounce, heading click scrolls, embedded page-index navigates via ui.openPage, and a label-restricted reader never sees the denied page in the index. - CI: the auth-e2e job now runs the section-styles (missed in #75), plugin-blocks, and page-tools packs, with login-rate-limit resets. - plugins.e2e.db.test clears the plugin registry up front: a local dev DB is shared with the e2e stack, whose installed real `toc` would otherwise collide with the fixture of the same id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import { createPlugin, windowTransport, type PageSummary } from '@dorfteich/plugin-sdk';
|
|
|
|
import de from '../i18n/de.json';
|
|
import en from '../i18n/en.json';
|
|
|
|
/**
|
|
* Page-index reference plugin (issue #77, plugin-architecture.md §Reference
|
|
* plugins): lists the pond's pages the viewer may read (`readPond` — the
|
|
* server filters per page, so permissions are inherently respected), with
|
|
* label chips to filter by. Clicking a page navigates via `ui.openPage`.
|
|
*
|
|
* Strings are bundled (the sandbox CSP blocks runtime fetches); the files in
|
|
* `i18n/` are the single source and are inlined at build time.
|
|
*/
|
|
const STRINGS: Record<string, Record<string, string>> = { de, en };
|
|
|
|
function labelFor(locale: string, key: string): string {
|
|
const base = locale.split('-')[0] ?? locale;
|
|
return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key;
|
|
}
|
|
|
|
const { host } = createPlugin({
|
|
transport: windowTransport({
|
|
// Window.postMessage's overloads don't structurally match the transport's
|
|
// minimal shape; this adapter pins the sandbox-safe wildcard origin.
|
|
target: { postMessage: (message) => window.parent.postMessage(message, '*') },
|
|
source: window,
|
|
}),
|
|
onRender: (context) => void render(context.locale),
|
|
});
|
|
|
|
/** The active label filter; `null` shows every readable page. */
|
|
let activeLabel: string | null = null;
|
|
|
|
async function render(locale: string): Promise<void> {
|
|
let pages: PageSummary[];
|
|
try {
|
|
pages = await host.readPond.listPages();
|
|
} catch {
|
|
document.body.textContent = labelFor(locale, 'error');
|
|
return;
|
|
}
|
|
|
|
document.body.textContent = '';
|
|
document.body.className = 'dt-page-index';
|
|
|
|
const labels = [...new Set(pages.flatMap((page) => page.labels))].sort();
|
|
if (labels.length > 0) {
|
|
document.body.appendChild(buildFilterBar(labels, locale));
|
|
}
|
|
|
|
const shown = activeLabel ? pages.filter((page) => page.labels.includes(activeLabel!)) : pages;
|
|
if (shown.length === 0) {
|
|
const empty = document.createElement('p');
|
|
empty.textContent = labelFor(locale, 'empty');
|
|
document.body.appendChild(empty);
|
|
} else {
|
|
const list = document.createElement('ul');
|
|
for (const page of shown) {
|
|
const item = document.createElement('li');
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.textContent = page.title;
|
|
link.dataset.pageId = page.id;
|
|
link.addEventListener('click', (event) => {
|
|
event.preventDefault();
|
|
void host.ui.openPage(page.id);
|
|
});
|
|
item.appendChild(link);
|
|
if (page.labels.length > 0) {
|
|
const chips = document.createElement('span');
|
|
chips.textContent = ` (${page.labels.join(', ')})`;
|
|
item.appendChild(chips);
|
|
}
|
|
list.appendChild(item);
|
|
}
|
|
document.body.appendChild(list);
|
|
}
|
|
|
|
void host.ui.resize(document.body.scrollHeight + 16);
|
|
}
|
|
|
|
function buildFilterBar(labels: string[], locale: string): HTMLElement {
|
|
const bar = document.createElement('p');
|
|
bar.className = 'dt-page-index__filters';
|
|
const options: Array<{ value: string | null; text: string }> = [
|
|
{ value: null, text: labelFor(locale, 'all') },
|
|
...labels.map((label) => ({ value: label, text: label })),
|
|
];
|
|
for (const option of options) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.textContent = option.text;
|
|
button.dataset.filter = option.value ?? '';
|
|
button.style.fontWeight = option.value === activeLabel ? 'bold' : 'normal';
|
|
button.addEventListener('click', () => {
|
|
activeLabel = option.value;
|
|
void render(locale);
|
|
});
|
|
bar.appendChild(button);
|
|
bar.appendChild(document.createTextNode(' '));
|
|
}
|
|
return bar;
|
|
}
|