Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createSubframePlaceholder } from "./index.js";
|
||||
|
||||
describe("@toolctrl/ui-subframe", () => {
|
||||
test("exports an empty subframe placeholder contract", () => {
|
||||
expect(createSubframePlaceholder()).toEqual({ status: "empty" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { DataTableModel, FileBrowserModel, TaskListModel } from "@toolctrl/view-models";
|
||||
|
||||
export const uiSubframeRegistry = [] as const;
|
||||
|
||||
export function createSubframePlaceholder() {
|
||||
return { status: "empty" } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real Subframe render implementation: turns a normalized {@link DataTableModel}
|
||||
* into a semantic HTML table carrying only structure, data-attributes and the
|
||||
* row cell data. No hardcoded user-facing copy — labels travel as i18n keys via
|
||||
* `data-i18n-key`, so the host app translates at hydration time.
|
||||
*/
|
||||
export function renderDataTable(model: DataTableModel): string {
|
||||
const head = model.columns.map((col) => `<th scope="col" data-i18n-key="${escapeHtml(col.labelKey)}" data-align="${escapeHtml(col.align)}"></th>`).join("");
|
||||
|
||||
const body =
|
||||
model.rowCount === 0
|
||||
? `<tr data-empty="true"><td colspan="${model.columns.length}" data-i18n-key="${escapeHtml(model.emptyLabelKey)}"></td></tr>`
|
||||
: model.rows
|
||||
.map((row) => {
|
||||
const cells = model.columns.map((col) => `<td data-align="${escapeHtml(col.align)}">${escapeHtml(row.cells[col.key] ?? "")}</td>`).join("");
|
||||
return `<tr data-row-id="${escapeHtml(row.id)}">${cells}</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const caption = model.captionKey ? `<caption data-i18n-key="${escapeHtml(model.captionKey)}"></caption>` : "";
|
||||
|
||||
return `<table data-ui-impl="subframe" data-ui-id="ui.data-table" data-row-count="${model.rowCount}">${caption}<thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real Subframe render implementation for the files browser. Semantic list with
|
||||
* per-row data-attributes; user-facing copy travels as i18n keys.
|
||||
*/
|
||||
export function renderFileBrowser(model: FileBrowserModel): string {
|
||||
const body =
|
||||
model.rowCount === 0
|
||||
? `<li data-empty="true" data-i18n-key="${escapeHtml(model.emptyHeadlineKey)}"></li>`
|
||||
: model.rows
|
||||
.map(
|
||||
(row) =>
|
||||
`<li data-row-id="${escapeHtml(row.id)}" data-mime="${escapeHtml(row.mimeType)}" data-size="${row.sizeBytes}"><a href="${escapeHtml(row.detailHref)}">${escapeHtml(row.displayName)}</a></li>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<section data-ui-impl="subframe" data-ui-id="ui.files.browser" data-row-count="${model.rowCount}"><h2 data-i18n-key="${escapeHtml(model.titleKey)}"></h2><ul>${body}</ul></section>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real Subframe render implementation for the reusable task list (AK-P4b). The
|
||||
* same model backs the standalone browser and the embedded entity "Tasks tab".
|
||||
* Semantic list with per-row data-attributes; user-facing copy travels as i18n
|
||||
* keys (status/priority), resolved assignee names are data.
|
||||
*/
|
||||
export function renderTaskList(model: TaskListModel): string {
|
||||
const body =
|
||||
model.rowCount === 0
|
||||
? `<li data-empty="true" data-i18n-key="${escapeHtml(model.emptyHeadlineKey)}"></li>`
|
||||
: model.rows
|
||||
.map(
|
||||
(row) =>
|
||||
`<li data-row-id="${escapeHtml(row.id)}" data-status-key="${escapeHtml(row.statusKey)}" data-priority-key="${escapeHtml(row.priorityKey)}" data-overdue="${row.isOverdue}"><a href="${escapeHtml(row.detailHref)}">${escapeHtml(row.title)}</a><span data-assignee>${escapeHtml(row.assigneeLabel ?? "")}</span></li>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<section data-ui-impl="subframe" data-ui-id="ui.tasks.list" data-row-count="${model.rowCount}"><h2 data-i18n-key="${escapeHtml(model.titleKey)}"></h2><ul>${body}</ul></section>`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { DataTableModel } from "@toolctrl/view-models";
|
||||
|
||||
import { renderDataTable } from "./index.js";
|
||||
|
||||
const model: DataTableModel = {
|
||||
captionKey: "ui.data-table.caption",
|
||||
columns: [
|
||||
{ key: "name", labelKey: "ui.data-table.column.name", align: "left" },
|
||||
{ key: "amount", labelKey: "ui.data-table.column.amount", align: "right" },
|
||||
],
|
||||
rows: [
|
||||
{ id: "row-1", cells: { name: "Alpha", amount: "10" } },
|
||||
{ id: "row-2", cells: { name: "Beta", amount: "20" } },
|
||||
],
|
||||
emptyLabelKey: "ui.data-table.empty",
|
||||
rowCount: 2,
|
||||
};
|
||||
|
||||
const emptyModel: DataTableModel = {
|
||||
columns: [{ key: "name", labelKey: "ui.data-table.column.name", align: "left" }],
|
||||
rows: [],
|
||||
emptyLabelKey: "ui.data-table.empty",
|
||||
rowCount: 0,
|
||||
};
|
||||
|
||||
describe("renderDataTable (subframe impl)", () => {
|
||||
test("renders a semantic table with the subframe implementation markers", () => {
|
||||
const html = renderDataTable(model);
|
||||
expect(html).toContain('<table data-ui-impl="subframe" data-ui-id="ui.data-table"');
|
||||
expect(html).toContain("</table>");
|
||||
expect(html).toContain("<thead>");
|
||||
expect(html).toContain("<tbody>");
|
||||
});
|
||||
|
||||
test("emits header cells with i18n keys and align attributes", () => {
|
||||
const html = renderDataTable(model);
|
||||
expect(html).toContain('data-i18n-key="ui.data-table.column.name"');
|
||||
expect(html).toContain('data-i18n-key="ui.data-table.column.amount"');
|
||||
expect(html).toContain('data-align="left"');
|
||||
expect(html).toContain('data-align="right"');
|
||||
});
|
||||
|
||||
test("renders a caption with its i18n key when captionKey is present", () => {
|
||||
const html = renderDataTable(model);
|
||||
expect(html).toContain('<caption data-i18n-key="ui.data-table.caption"></caption>');
|
||||
});
|
||||
|
||||
test("renders rows with row ids and escaped cell data", () => {
|
||||
const html = renderDataTable(model);
|
||||
expect(html).toContain('data-row-id="row-1"');
|
||||
expect(html).toContain('data-row-id="row-2"');
|
||||
expect(html).toContain(">Alpha<");
|
||||
expect(html).toContain(">20<");
|
||||
});
|
||||
|
||||
test("renders an empty row with the empty i18n key when there are no rows", () => {
|
||||
const html = renderDataTable(emptyModel);
|
||||
expect(html).toContain('data-empty="true"');
|
||||
expect(html).toContain('data-i18n-key="ui.data-table.empty"');
|
||||
expect(html).not.toContain("data-row-id=");
|
||||
});
|
||||
|
||||
test("escapes hostile cell content", () => {
|
||||
const html = renderDataTable({
|
||||
...model,
|
||||
rows: [{ id: "row-x", cells: { name: '<script>"&"</script>', amount: "" } }],
|
||||
rowCount: 1,
|
||||
});
|
||||
expect(html).not.toContain("<script>");
|
||||
expect(html).toContain("<script>");
|
||||
});
|
||||
|
||||
test("is visually stable: identical input renders identical output (M016)", () => {
|
||||
expect(renderDataTable(model)).toBe(renderDataTable(model));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user