@kahitsan/ksui 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahitsan/ksui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "ksui is a standalone set of SolidJS UI components for KahitSan/Hilinga and any SolidJS app. Published to the public npm registry and consumed as a normal dependency. Ships source under a `solid` export condition so the consumer's vite-plugin-solid compiles it with only solid-js externalized; it depends on nothing but solid-js + lucide-solid and injects its own CSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -48,4 +48,14 @@ describe("DataTable (client-side mode)", () => {
|
|
|
48
48
|
));
|
|
49
49
|
expect(screen.getByPlaceholderText("Filter...")).toBeTruthy();
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
it("respects pageLength: only that many rows render on page 1 (client-side)", () => {
|
|
53
|
+
render(() => (
|
|
54
|
+
<DataTable columns={COLUMNS} data={DATA} paging={true} pageLength={2} />
|
|
55
|
+
));
|
|
56
|
+
// page size 2 over 3 rows → page 1 shows the first two, the third is on page 2.
|
|
57
|
+
expect(screen.getByText("Alpha")).toBeTruthy();
|
|
58
|
+
expect(screen.getByText("Beta")).toBeTruthy();
|
|
59
|
+
expect(screen.queryByText("Gamma")).toBeNull();
|
|
60
|
+
});
|
|
51
61
|
});
|
|
@@ -294,6 +294,8 @@ export function ResourcePage<T extends ResourceRow>(
|
|
|
294
294
|
searching={true}
|
|
295
295
|
ordering={true}
|
|
296
296
|
paging={true}
|
|
297
|
+
pageLength={spec.pageLength}
|
|
298
|
+
lengthMenu={spec.lengthMenu ? [...spec.lengthMenu] : undefined}
|
|
297
299
|
searchPlaceholder={spec.labels.searchPlaceholder}
|
|
298
300
|
emptyMessage={spec.labels.empty}
|
|
299
301
|
noResultsMessage={spec.labels.noResults}
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
selectDefault,
|
|
12
12
|
cleanLabel,
|
|
13
13
|
validateForm,
|
|
14
|
+
resolveForeignValue,
|
|
14
15
|
type ResourceUiSpec,
|
|
15
16
|
type UiFieldSelect,
|
|
17
|
+
type UiColumn,
|
|
16
18
|
} from "./spec";
|
|
17
19
|
|
|
18
20
|
// A self-contained fixture exercising every helper branch (segmented + select
|
|
@@ -114,3 +116,25 @@ describe("formToBody", () => {
|
|
|
114
116
|
.toEqual({ name: "X", kind: "customer", category: null, notes: "n" });
|
|
115
117
|
});
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
describe("resolveForeignValue (U4 foreign-data contract)", () => {
|
|
121
|
+
const plain: UiColumn = { key: "name", title: "Name", render: { type: "text" } };
|
|
122
|
+
const foreign: UiColumn = {
|
|
123
|
+
key: "balance",
|
|
124
|
+
title: "Balance",
|
|
125
|
+
render: { type: "text" },
|
|
126
|
+
foreign: { source: { peer: "financial-accounts", field: "balance" }, onError: "dash" },
|
|
127
|
+
};
|
|
128
|
+
const row = { id: 1, name: "Acme", balance: "ignored-own-value" };
|
|
129
|
+
|
|
130
|
+
it("reads the own row value for a plain column", () => {
|
|
131
|
+
expect(resolveForeignValue(plain, row)).toBe("Acme");
|
|
132
|
+
});
|
|
133
|
+
it("THROWS for a foreign column with no resolver wired (throw-on-unwired guard)", () => {
|
|
134
|
+
expect(() => resolveForeignValue(foreign, row)).toThrow(/foreign source.*no ForeignResolver/);
|
|
135
|
+
});
|
|
136
|
+
it("delegates to a wired resolver for a foreign column", () => {
|
|
137
|
+
const resolver = (s: { peer: string; field: string }) => `${s.peer}:${s.field}=42`;
|
|
138
|
+
expect(resolveForeignValue(foreign, row, resolver)).toBe("financial-accounts:balance=42");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -42,6 +42,63 @@ export interface UiColumn {
|
|
|
42
42
|
readonly title: string;
|
|
43
43
|
readonly orderable?: boolean;
|
|
44
44
|
readonly render: UiColumnRender;
|
|
45
|
+
/**
|
|
46
|
+
* Optional: the value is read from a PEER plugin, not this resource's own row
|
|
47
|
+
* (see UiForeignColumn). A foreign column is never sortable server-side.
|
|
48
|
+
*/
|
|
49
|
+
readonly foreign?: UiForeignColumn;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- U4: foreign data sources (declarative; resolved via the host consent model) ----
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Declares that a column's value is sourced from a PEER plugin rather than the
|
|
56
|
+
* resource's own row. The cross-plugin read is mediated by the host's consent model
|
|
57
|
+
* (kernel IP1). This is a FORWARD CONTRACT: a spec may declare a foreign source before
|
|
58
|
+
* the runtime can serve it — `resolveForeignValue` throws until a resolver is wired,
|
|
59
|
+
* so a missing consent path fails loud, never silently renders an empty cell.
|
|
60
|
+
*/
|
|
61
|
+
export interface UiForeignSource {
|
|
62
|
+
/** The peer plugin that owns the value (e.g. "financial-accounts"). */
|
|
63
|
+
readonly peer: string;
|
|
64
|
+
/** The field on the peer record this column reads. */
|
|
65
|
+
readonly field: string;
|
|
66
|
+
/** Opaque id on THIS row used to join to the peer (defaults to the column key). */
|
|
67
|
+
readonly joinKey?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** How a foreign column degrades when the peer read is unavailable/slow/denied. */
|
|
71
|
+
export type UiForeignOnError = "hide" | "dash" | "warn";
|
|
72
|
+
|
|
73
|
+
/** A column whose value comes from a peer plugin via the consent model. */
|
|
74
|
+
export interface UiForeignColumn {
|
|
75
|
+
readonly source: UiForeignSource;
|
|
76
|
+
/** Degrade policy when the peer read fails — never breaks the row. Defaults to "dash". */
|
|
77
|
+
readonly onError?: UiForeignOnError;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The consent-gated peer-read seam the host supplies (kernel IP1). */
|
|
81
|
+
export type ForeignResolver = (source: UiForeignSource, row: ResourceRow) => unknown;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a column's value, honouring a declared foreign source. A plain column reads
|
|
85
|
+
* `row[key]`. A foreign column requires a wired `ForeignResolver` — without one it
|
|
86
|
+
* THROWS (the throw-on-unwired guard), because foreign reads cannot resolve until the
|
|
87
|
+
* host consent model exists. Callers that catch this apply the column's `onError`.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveForeignValue(
|
|
90
|
+
col: UiColumn,
|
|
91
|
+
row: ResourceRow,
|
|
92
|
+
resolver?: ForeignResolver,
|
|
93
|
+
): unknown {
|
|
94
|
+
if (!col.foreign) return row[col.key];
|
|
95
|
+
if (!resolver) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`ksui: column "${col.key}" declares a foreign source (peer "${col.foreign.source.peer}") ` +
|
|
98
|
+
`but no ForeignResolver is wired — foreign reads require the host consent model.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return resolver(col.foreign.source, row);
|
|
45
102
|
}
|
|
46
103
|
|
|
47
104
|
// ---- form fields -----------------------------------------------------------
|
|
@@ -156,6 +213,13 @@ export interface ResourceUiSpec {
|
|
|
156
213
|
};
|
|
157
214
|
/** data-testid prefix (e.g. "things" → things-add-btn, things-row-3). */
|
|
158
215
|
readonly testIdPrefix: string;
|
|
216
|
+
/** Initial rows-per-page for the list (DataTable `pageLength`). When omitted,
|
|
217
|
+
* DataTable's own default (10) applies. A host can lower a resolved user/workspace
|
|
218
|
+
* preference in here (the platform's route-settings `pageSize`). */
|
|
219
|
+
readonly pageLength?: number;
|
|
220
|
+
/** The rows-per-page options offered in the list's page-size menu (DataTable
|
|
221
|
+
* `lengthMenu`). When omitted, DataTable's default ([10, 25, 50, 100]) applies. */
|
|
222
|
+
readonly lengthMenu?: readonly number[];
|
|
159
223
|
}
|
|
160
224
|
|
|
161
225
|
// ---- derived endpoints -----------------------------------------------------
|