@gaia-ai/addon-gaia-ui 0.6.5 → 0.8.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/README.md +1 -1
- package/dist/src/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/section-list/index.d.ts +37 -0
- package/dist/src/Component/section-list/index.js +60 -13
- package/dist/src/Component/stats-panel/index.d.ts +60 -0
- package/dist/src/Component/stats-panel/index.js +124 -0
- package/dist/src/Component/top-tabs/index.d.ts +18 -11
- package/dist/src/Component/top-tabs/index.js +34 -26
- package/dist/src/Entity/collection.d.ts +98 -0
- package/dist/src/Entity/collection.js +46 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +66 -3
- package/dist/src/Entity/conductor/Data/conductors.js +76 -3
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +42 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +200 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Component/project-list/index.d.ts +8 -0
- package/dist/src/Entity/project/Component/project-list/index.js +7 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +9 -6
- package/dist/src/Entity/project/Data/projects.js +7 -4
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +36 -0
- package/dist/src/Entity/project/Screen/project-picker.js +90 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +40 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +103 -0
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +17 -0
- package/dist/src/Entity/run/Component/run-detail/index.js +145 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +122 -4
- package/dist/src/Entity/run/Data/runs.js +205 -26
- package/dist/src/Entity/run/Screen/run-detail.d.ts +25 -0
- package/dist/src/Entity/run/Screen/run-detail.js +51 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +82 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Data/statistics.d.ts +87 -0
- package/dist/src/Entity/statistics/Data/statistics.js +144 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +42 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +177 -0
- package/dist/src/Entity/term/Data/terms.d.ts +9 -2
- package/dist/src/Entity/term/Data/terms.js +9 -3
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -26
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +24 -2
- package/dist/src/Entity/ticket/Component/create-form/index.js +61 -23
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +4 -3
- package/dist/src/Entity/ticket/Component/edit-form/index.js +68 -48
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +32 -5
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +47 -6
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +268 -16
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Data/detail.d.ts +16 -0
- package/dist/src/Entity/ticket/Data/detail.js +17 -1
- package/dist/src/Entity/ticket/Data/parents.d.ts +39 -0
- package/dist/src/Entity/ticket/Data/parents.js +119 -0
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +120 -4
- package/dist/src/Entity/ticket/Data/tickets.js +178 -12
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +45 -0
- package/dist/src/Entity/ticket/Form/create-data.js +120 -20
- package/dist/src/Entity/ticket/Form/create.d.ts +106 -5
- package/dist/src/Entity/ticket/Form/create.js +86 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +68 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +336 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +177 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +45 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +135 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +61 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +300 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +507 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +154 -0
- package/dist/src/Entity/transport.d.ts +30 -0
- package/dist/src/Entity/transport.js +27 -5
- package/dist/src/Entity/user/Data/users.d.ts +29 -3
- package/dist/src/Entity/user/Data/users.js +77 -8
- package/dist/src/Form/field.d.ts +64 -2
- package/dist/src/Form/field.js +12 -0
- package/dist/src/Form/form.d.ts +14 -1
- package/dist/src/Form/form.js +118 -9
- package/dist/src/Form/keys.d.ts +5 -1
- package/dist/src/Form/keys.js +54 -4
- package/dist/src/Kernel/tui-kernel.d.ts +1 -0
- package/dist/src/Kernel/tui-kernel.js +256 -1536
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +39 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +33 -0
- package/dist/src/Routing/router.js +102 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +50 -0
- package/dist/src/Screen/async-screen.js +164 -0
- package/dist/src/Screen/list-screen.d.ts +158 -0
- package/dist/src/Screen/list-screen.js +237 -0
- package/dist/src/Screen/projects.d.ts +1 -0
- package/dist/src/Screen/projects.js +1 -0
- package/dist/src/Screen/screen.d.ts +106 -0
- package/dist/src/Screen/screen.js +18 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/action.d.ts +3 -0
- package/dist/src/Widget/action.js +9 -0
- package/dist/src/Widget/autocomplete.d.ts +3 -0
- package/dist/src/Widget/autocomplete.js +80 -0
- package/dist/src/Widget/chrome.d.ts +38 -0
- package/dist/src/Widget/chrome.js +60 -0
- package/dist/src/Widget/index.d.ts +4 -0
- package/dist/src/Widget/index.js +12 -0
- package/dist/src/Widget/multiselect.d.ts +3 -0
- package/dist/src/Widget/multiselect.js +36 -0
- package/dist/src/Widget/option-source.d.ts +58 -0
- package/dist/src/Widget/option-source.js +98 -0
- package/dist/src/Widget/render.d.ts +42 -0
- package/dist/src/Widget/render.js +82 -0
- package/dist/src/Widget/select.d.ts +3 -0
- package/dist/src/Widget/select.js +26 -0
- package/dist/src/Widget/text.d.ts +3 -0
- package/dist/src/Widget/text.js +68 -0
- package/dist/src/Widget/types.d.ts +60 -0
- package/dist/src/Widget/types.js +21 -0
- package/dist/src/launcher.d.ts +12 -4
- package/dist/src/launcher.js +5 -4
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +59 -0
- package/dist/src/lib/hit-test.d.ts +39 -20
- package/dist/src/lib/hit-test.js +31 -22
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/width.d.ts +69 -1
- package/dist/src/lib/width.js +247 -5
- package/dist/src/plugin.js +21 -2
- package/dist/src/route.js +2 -2
- package/dist/src/types.d.ts +42 -2
- package/dist/src/types.js +0 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @gaia-ai/addon-gaia-ui
|
|
2
2
|
|
|
3
|
-
GAIA TUI: interactive terminal UI (
|
|
3
|
+
GAIA TUI: interactive terminal UI (dashboard, projects, tickets) as a dropsh `tui` renderer plugin.
|
|
4
4
|
|
|
5
5
|
Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
|
|
@@ -17,9 +17,9 @@ export declare function buildCreateLaunch(opts: {
|
|
|
17
17
|
label?: string | undefined;
|
|
18
18
|
}): CreateLaunch;
|
|
19
19
|
/**
|
|
20
|
-
* A short human label for the agent list: the
|
|
21
|
-
* incl. the ellipsis), else
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* A short human label for the agent list: the ticket's title, else the
|
|
21
|
+
* description's first line (both capped to 40 incl. the ellipsis), else
|
|
22
|
+
* "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
|
|
23
|
+
* takes, so it belongs to the module that builds the launch intent.
|
|
24
24
|
*/
|
|
25
25
|
export declare function shortName(payload: CreateFormPayload): string;
|
package/dist/src/Agent/launch.js
CHANGED
|
@@ -32,18 +32,25 @@ export function buildCreateLaunch(opts) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* A short human label for the agent list: the
|
|
36
|
-
* incl. the ellipsis), else
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* A short human label for the agent list: the ticket's title, else the
|
|
36
|
+
* description's first line (both capped to 40 incl. the ellipsis), else
|
|
37
|
+
* "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
|
|
38
|
+
* takes, so it belongs to the module that builds the launch intent.
|
|
39
39
|
*/
|
|
40
40
|
export function shortName(payload) {
|
|
41
|
+
// GAIA-309 D9: the title is what the operator NAMED the ticket, so it is the
|
|
42
|
+
// better tab label; the description's first line stays the fallback for a
|
|
43
|
+
// payload that has no title yet. This touches the launch intent's `label`
|
|
44
|
+
// ONLY — never `prompt`, never `env`, which is what AC-6 pins.
|
|
45
|
+
const cap = (line) => line.length > 40 ? `${line.slice(0, 39)}…` : line;
|
|
46
|
+
const title = payload.title.trim();
|
|
47
|
+
if (title)
|
|
48
|
+
return cap(title);
|
|
41
49
|
const firstLine = payload.description
|
|
42
50
|
.split('\n')
|
|
43
51
|
.map((l) => l.trim())
|
|
44
52
|
.find((l) => l.length > 0);
|
|
45
|
-
if (firstLine)
|
|
46
|
-
return firstLine
|
|
47
|
-
}
|
|
53
|
+
if (firstLine)
|
|
54
|
+
return cap(firstLine);
|
|
48
55
|
return `New ${payload.workflow?.label ?? 'ticket'}`;
|
|
49
56
|
}
|
|
@@ -7,3 +7,40 @@ export declare function flattenSections(sections: Section[]): {
|
|
|
7
7
|
lines: string[];
|
|
8
8
|
boldRows: Set<number>;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* One collapsible entry: a caret header row plus the rows shown in each state.
|
|
12
|
+
*
|
|
13
|
+
* GAIA-255 D2: the Comments tab's collapse/expand interaction and the Overview
|
|
14
|
+
* tab's Description are ONE primitive rather than two renderers that happen to
|
|
15
|
+
* agree — AC-2 asks for "the same expand interaction the Comments tab offers",
|
|
16
|
+
* and structural sharing is the only form of that claim a test can hold.
|
|
17
|
+
*/
|
|
18
|
+
export interface ExpandableEntry {
|
|
19
|
+
/** The header row's text, after the caret. */
|
|
20
|
+
header: string;
|
|
21
|
+
/** Rows shown while collapsed (indented like the expanded body). */
|
|
22
|
+
collapsed: string[];
|
|
23
|
+
/** Rows shown while expanded. */
|
|
24
|
+
expanded: string[];
|
|
25
|
+
}
|
|
26
|
+
/** The rendered rows of a list of expandable entries. */
|
|
27
|
+
export interface ExpandableView {
|
|
28
|
+
lines: string[];
|
|
29
|
+
boldRows: Set<number>;
|
|
30
|
+
/** `headerRows[i]` is the row index of entry i's header — the selection map. */
|
|
31
|
+
headerRows: number[];
|
|
32
|
+
selectable: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Flatten expandable entries: one caret header row per entry (`▸` collapsed,
|
|
36
|
+
* `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
|
|
37
|
+
* header row, which is both the highlight target and the click map (the rows are
|
|
38
|
+
* NOT uniformly spaced — an expanded entry appends its body).
|
|
39
|
+
*
|
|
40
|
+
* Zero entries render `emptyMessage` and nothing selectable, so an empty list is
|
|
41
|
+
* an explicit statement rather than a blank screen.
|
|
42
|
+
*/
|
|
43
|
+
export declare function flattenExpandable(entries: ExpandableEntry[], { expanded, emptyMessage, }: {
|
|
44
|
+
expanded?: ReadonlySet<number>;
|
|
45
|
+
emptyMessage: string;
|
|
46
|
+
}): ExpandableView;
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append one body row, applying the MINIMAL MARKDOWN rule: an ATX heading
|
|
3
|
+
* (`#`…`######`) renders as bold with the markers stripped — the same rule a
|
|
4
|
+
* section heading follows, styled after fit(). Bullets and code fences stay raw
|
|
5
|
+
* by design (explicit v1 decision).
|
|
6
|
+
*
|
|
7
|
+
* Extracted so `flattenSections` and `flattenExpandable` cannot answer "what does
|
|
8
|
+
* a `## Context` line render as" differently: the Description block is rendered
|
|
9
|
+
* by BOTH (flat on the piped render, expandable on the Overview tab), and a rule
|
|
10
|
+
* living in only one of them would make the same body look different depending on
|
|
11
|
+
* which renderer reached it.
|
|
12
|
+
*/
|
|
13
|
+
function pushRow(lines, boldRows, line) {
|
|
14
|
+
const md = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
15
|
+
if (md) {
|
|
16
|
+
boldRows.add(lines.length);
|
|
17
|
+
lines.push(md[2] ?? '');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
lines.push(line);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
1
23
|
// Flatten `{heading, lines}` sections into plain rows plus the indices that are
|
|
2
24
|
// bold (section headings + markdown ATX headings). Headings carry NO ANSI here —
|
|
3
25
|
// the draw step applies bold after fit(), so sanitizeDisplay never sees (and
|
|
@@ -8,20 +30,45 @@ export function flattenSections(sections) {
|
|
|
8
30
|
for (const section of sections) {
|
|
9
31
|
boldRows.add(lines.length);
|
|
10
32
|
lines.push(section.heading);
|
|
11
|
-
for (const line of section.lines)
|
|
12
|
-
|
|
13
|
-
// markers stripped — same rule as a section heading, styled after fit().
|
|
14
|
-
// Bullets and code fences stay raw by design (explicit v1 decision).
|
|
15
|
-
const md = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
16
|
-
if (md) {
|
|
17
|
-
boldRows.add(lines.length);
|
|
18
|
-
lines.push(md[2] ?? '');
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
lines.push(line);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
33
|
+
for (const line of section.lines)
|
|
34
|
+
pushRow(lines, boldRows, line);
|
|
24
35
|
lines.push('');
|
|
25
36
|
}
|
|
26
37
|
return { lines, boldRows };
|
|
27
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Flatten expandable entries: one caret header row per entry (`▸` collapsed,
|
|
41
|
+
* `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
|
|
42
|
+
* header row, which is both the highlight target and the click map (the rows are
|
|
43
|
+
* NOT uniformly spaced — an expanded entry appends its body).
|
|
44
|
+
*
|
|
45
|
+
* Zero entries render `emptyMessage` and nothing selectable, so an empty list is
|
|
46
|
+
* an explicit statement rather than a blank screen.
|
|
47
|
+
*/
|
|
48
|
+
export function flattenExpandable(entries, { expanded = new Set(), emptyMessage,
|
|
49
|
+
// GAIA-255 AC-11 review: `ReadonlySet` because the render path is handed ONE
|
|
50
|
+
// shared empty set for every not-yet-expanded tab (`expandedFor`) — growing
|
|
51
|
+
// it here would leak one tab's expansion into all the others. The renderer
|
|
52
|
+
// only ever calls `.has()`, so the narrowing costs it nothing.
|
|
53
|
+
}) {
|
|
54
|
+
if (!entries.length) {
|
|
55
|
+
return {
|
|
56
|
+
lines: [emptyMessage],
|
|
57
|
+
boldRows: new Set(),
|
|
58
|
+
headerRows: [],
|
|
59
|
+
selectable: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const lines = [];
|
|
63
|
+
const boldRows = new Set();
|
|
64
|
+
const headerRows = [];
|
|
65
|
+
entries.forEach((entry, i) => {
|
|
66
|
+
headerRows.push(lines.length);
|
|
67
|
+
boldRows.add(lines.length);
|
|
68
|
+
lines.push(`${expanded.has(i) ? '▾' : '▸'} ${entry.header}`);
|
|
69
|
+
for (const line of expanded.has(i) ? entry.expanded : entry.collapsed) {
|
|
70
|
+
pushRow(lines, boldRows, line);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return { lines, boldRows, headerRows, selectable: entries.length };
|
|
74
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** A filled bar cell. U+2588 FULL BLOCK. */
|
|
2
|
+
export declare const BAR_FULL = "\u2588";
|
|
3
|
+
/** An empty bar cell. U+2591 LIGHT SHADE. */
|
|
4
|
+
export declare const BAR_EMPTY = "\u2591";
|
|
5
|
+
/**
|
|
6
|
+
* The bar's width in display columns.
|
|
7
|
+
*
|
|
8
|
+
* `█`/`░` are narrow per `lib/width-table.ts` — neither is in `WIDE_RANGES` — so
|
|
9
|
+
* the row geometry GAIA-241's hit testing depends on is unaffected. They are NOT
|
|
10
|
+
* ASCII, though, and both are East-Asian-AMBIGUOUS: a terminal configured
|
|
11
|
+
* ambiguous-as-wide (common in CJK setups) renders them two columns and shifts
|
|
12
|
+
* every number column at runtime while every test here stays green, because the
|
|
13
|
+
* width table deliberately treats Ambiguous as narrow (the frame's box-drawing
|
|
14
|
+
* characters depend on that). Accepted rather than mitigated — spec R9. If it
|
|
15
|
+
* ever bites, the fix is an ASCII bar glyph, never a width-table change.
|
|
16
|
+
*/
|
|
17
|
+
export declare const BAR_WIDTH = 20;
|
|
18
|
+
/** One row of a block: a name and its counts, most-significant number last. */
|
|
19
|
+
export interface StatsPanelRow {
|
|
20
|
+
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* The counts to print, in column order. The LAST one is what the bar
|
|
23
|
+
* measures — for a breakdown row that is the month, so a row whose day count
|
|
24
|
+
* is zero still shows the activity it actually had.
|
|
25
|
+
*/
|
|
26
|
+
numbers: number[];
|
|
27
|
+
}
|
|
28
|
+
export interface StatsPanelOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Column titles, printed as a header row above the numbers. Omitted for a
|
|
31
|
+
* single-number block, where the label already says which window it is.
|
|
32
|
+
*
|
|
33
|
+
* The mock in spec D11 hangs these titles off the block's HEADING line; they
|
|
34
|
+
* are their own row here because the heading belongs to `flattenSections`,
|
|
35
|
+
* which owns the bold-row set. Same information, one row lower, and the two
|
|
36
|
+
* components keep their boundary.
|
|
37
|
+
*/
|
|
38
|
+
columns?: readonly string[] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The group's name, printed in the header row's already-empty label field
|
|
41
|
+
* (GAIA-324 D4). A caller that renders several groups under one heading uses
|
|
42
|
+
* it to say which group each block of bars is.
|
|
43
|
+
*
|
|
44
|
+
* The component stays ENTITY-NEUTRAL: this is a string, and nothing here knows
|
|
45
|
+
* a ticket from a run. It is padded exactly like a data label, so the title,
|
|
46
|
+
* the labels beneath it and the column titles keep one geometry.
|
|
47
|
+
*/
|
|
48
|
+
title?: string | undefined;
|
|
49
|
+
/** What to render instead of rows when there are none (spec R7). */
|
|
50
|
+
emptyMessage?: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One block's body rows: an optional header row (the group's title, the column
|
|
54
|
+
* titles, or both), then one row per entry.
|
|
55
|
+
*
|
|
56
|
+
* Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
|
|
57
|
+
* wide-character label cannot shift the numbers out of alignment — the failure
|
|
58
|
+
* that makes a table of counts unreadable exactly when the data is interesting.
|
|
59
|
+
*/
|
|
60
|
+
export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, title, emptyMessage }?: StatsPanelOptions): string[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { pad } from '../../lib/format.js';
|
|
2
|
+
import { displayWidth } from '../../lib/width.js';
|
|
3
|
+
// GAIA-256 spec D11 — the Dashboard's `/usage`-style bar rows.
|
|
4
|
+
//
|
|
5
|
+
// A SHARED component (the third in `src/Component/`), and it earns that place by
|
|
6
|
+
// carrying no entity vocabulary at all: it takes `{ label, numbers }` and draws
|
|
7
|
+
// bars. Nothing here knows a ticket from a run from a project — which is exactly
|
|
8
|
+
// what G4 asserts, and why the Dashboard can render four different blocks from
|
|
9
|
+
// one function.
|
|
10
|
+
//
|
|
11
|
+
// It produces the ROWS OF ONE BLOCK. Stacking blocks under bold headings is
|
|
12
|
+
// `flattenSections`' job (`Component/section-list`), unchanged: this component
|
|
13
|
+
// only fills a block's body.
|
|
14
|
+
/** A filled bar cell. U+2588 FULL BLOCK. */
|
|
15
|
+
export const BAR_FULL = '█';
|
|
16
|
+
/** An empty bar cell. U+2591 LIGHT SHADE. */
|
|
17
|
+
export const BAR_EMPTY = '░';
|
|
18
|
+
/**
|
|
19
|
+
* The bar's width in display columns.
|
|
20
|
+
*
|
|
21
|
+
* `█`/`░` are narrow per `lib/width-table.ts` — neither is in `WIDE_RANGES` — so
|
|
22
|
+
* the row geometry GAIA-241's hit testing depends on is unaffected. They are NOT
|
|
23
|
+
* ASCII, though, and both are East-Asian-AMBIGUOUS: a terminal configured
|
|
24
|
+
* ambiguous-as-wide (common in CJK setups) renders them two columns and shifts
|
|
25
|
+
* every number column at runtime while every test here stays green, because the
|
|
26
|
+
* width table deliberately treats Ambiguous as narrow (the frame's box-drawing
|
|
27
|
+
* characters depend on that). Accepted rather than mitigated — spec R9. If it
|
|
28
|
+
* ever bites, the fix is an ASCII bar glyph, never a width-table change.
|
|
29
|
+
*/
|
|
30
|
+
export const BAR_WIDTH = 20;
|
|
31
|
+
/**
|
|
32
|
+
* The label column, in display columns: 26 for the text plus {@link LABEL_GAP}.
|
|
33
|
+
*
|
|
34
|
+
* The text budget is what the field has to hold, and after GAIA-324 that is two
|
|
35
|
+
* different kinds of text sharing one column: a breakdown row's bare entity name
|
|
36
|
+
* (a real project name reaches 26 columns on its own — `gaia-e2e-project` plus a
|
|
37
|
+
* suffix used to; a long one still does), and the group TITLE printed in the
|
|
38
|
+
* header row's label field above it. Both are padded to the same field, which is
|
|
39
|
+
* what keeps a title, the labels it captions and the column titles on one
|
|
40
|
+
* geometry. Kept at 26 rather than narrowed with the `· tickets` suffix GAIA-324
|
|
41
|
+
* deleted: the budget was never sized for the suffix alone, and a project name
|
|
42
|
+
* cut to `a-very-long-project-…` is the same unreadable row the width exists to
|
|
43
|
+
* prevent.
|
|
44
|
+
*/
|
|
45
|
+
const LABEL_WIDTH = 27;
|
|
46
|
+
/**
|
|
47
|
+
* Columns of the label field reserved as the gap before the bar.
|
|
48
|
+
*
|
|
49
|
+
* Load-bearing, not decoration. `pad` fills a label to exactly `LABEL_WIDTH`, so
|
|
50
|
+
* a label of precisely that width left NO separator and the row rendered
|
|
51
|
+
* `gaia-e2e-project-name██████` — a real project name, at exactly 26 columns. A
|
|
52
|
+
* truncated label collided the same way, ending in `…` against the bar. Every
|
|
53
|
+
* fixture in the tests was shorter, which is why the suite was green while the
|
|
54
|
+
* screen was wrong; the gap is part of the geometry now so the label length
|
|
55
|
+
* cannot decide whether it exists.
|
|
56
|
+
*
|
|
57
|
+
* It is ADDED to the field rather than taken out of it: the 26 columns above are
|
|
58
|
+
* a content budget with a stated reason, and spending one of them on the gap
|
|
59
|
+
* would truncate one more column off the longest names — the labels that are
|
|
60
|
+
* already hardest to tell apart.
|
|
61
|
+
*/
|
|
62
|
+
const LABEL_GAP = 1;
|
|
63
|
+
/** One number column, in display columns. */
|
|
64
|
+
const NUMBER_WIDTH = 8;
|
|
65
|
+
/** The indent every row of a block carries, matching the other body renderers. */
|
|
66
|
+
const INDENT = ' ';
|
|
67
|
+
/** `value` right-aligned to `width` DISPLAY columns — the number columns' rule. */
|
|
68
|
+
function padNumber(value, width) {
|
|
69
|
+
const gap = Math.max(0, width - displayWidth(value));
|
|
70
|
+
return ' '.repeat(gap) + value;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The bar for `value` within a group whose largest bar value is `max`.
|
|
74
|
+
*
|
|
75
|
+
* Scaled RELATIVE TO THE LARGEST VALUE IN ITS OWN GROUP, not to a limit. This is
|
|
76
|
+
* the one place the `/usage` analogy does not carry: `/usage` measures against a
|
|
77
|
+
* quota, and there is no quota here — so a full bar means "the biggest of these",
|
|
78
|
+
* never "100 %". An all-zero group has no maximum to scale by and renders empty
|
|
79
|
+
* rather than dividing by zero.
|
|
80
|
+
*/
|
|
81
|
+
function bar(value, max) {
|
|
82
|
+
const filled = max > 0
|
|
83
|
+
? Math.max(0, Math.min(BAR_WIDTH, Math.round((value / max) * BAR_WIDTH)))
|
|
84
|
+
: 0;
|
|
85
|
+
return BAR_FULL.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One block's body rows: an optional header row (the group's title, the column
|
|
89
|
+
* titles, or both), then one row per entry.
|
|
90
|
+
*
|
|
91
|
+
* Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
|
|
92
|
+
* wide-character label cannot shift the numbers out of alignment — the failure
|
|
93
|
+
* that makes a table of counts unreadable exactly when the data is interesting.
|
|
94
|
+
*/
|
|
95
|
+
export function statsPanelRows(rows, { columns, title, emptyMessage } = {}) {
|
|
96
|
+
if (rows.length === 0) {
|
|
97
|
+
return emptyMessage === undefined ? [] : [`${INDENT}${emptyMessage}`];
|
|
98
|
+
}
|
|
99
|
+
const barValue = (row) => row.numbers.at(-1) ?? 0;
|
|
100
|
+
const max = Math.max(...rows.map(barValue));
|
|
101
|
+
const out = [];
|
|
102
|
+
if ((columns && columns.length > 0) || title !== undefined) {
|
|
103
|
+
out.push(INDENT +
|
|
104
|
+
// The SAME label geometry as a data row below (padded to the field width
|
|
105
|
+
// minus the gap, then the gap), so a group title sits exactly over the
|
|
106
|
+
// labels it captions. With no title this is the blank label field the
|
|
107
|
+
// header row always had, byte for byte.
|
|
108
|
+
pad(title ?? '', LABEL_WIDTH - LABEL_GAP) +
|
|
109
|
+
' '.repeat(LABEL_GAP) +
|
|
110
|
+
' '.repeat(BAR_WIDTH) +
|
|
111
|
+
(columns ?? []).map((c) => padNumber(c, NUMBER_WIDTH)).join(''));
|
|
112
|
+
}
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
out.push(INDENT +
|
|
115
|
+
// Padded to the field width MINUS the gap, then the gap: the label
|
|
116
|
+
// column stays `LABEL_WIDTH` wide either way, so the header row above
|
|
117
|
+
// and every bar below still line up.
|
|
118
|
+
pad(row.label, LABEL_WIDTH - LABEL_GAP) +
|
|
119
|
+
' '.repeat(LABEL_GAP) +
|
|
120
|
+
bar(barValue(row), max) +
|
|
121
|
+
row.numbers.map((n) => padNumber(String(n), NUMBER_WIDTH)).join(''));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
@@ -5,11 +5,20 @@ export interface TopTab {
|
|
|
5
5
|
}
|
|
6
6
|
/** The root level: what there is before a project is chosen. */
|
|
7
7
|
export declare const ROOT_TABS: readonly [{
|
|
8
|
+
readonly id: "dashboard";
|
|
9
|
+
readonly label: "Dashboard";
|
|
10
|
+
}, {
|
|
8
11
|
readonly id: "projects";
|
|
9
12
|
readonly label: "Projects";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "conductors";
|
|
15
|
+
readonly label: "Conductors";
|
|
10
16
|
}];
|
|
11
17
|
/** The project level: the tabs of an opened project. */
|
|
12
18
|
export declare const PROJECT_TABS: readonly [{
|
|
19
|
+
readonly id: "dashboard";
|
|
20
|
+
readonly label: "Dashboard";
|
|
21
|
+
}, {
|
|
13
22
|
readonly id: "tickets";
|
|
14
23
|
readonly label: "Tickets";
|
|
15
24
|
}, {
|
|
@@ -22,10 +31,19 @@ export declare const PROJECT_TABS: readonly [{
|
|
|
22
31
|
/** The tab bars, keyed by level — the one place a level is named. */
|
|
23
32
|
export declare const TAB_LEVELS: {
|
|
24
33
|
readonly root: readonly [{
|
|
34
|
+
readonly id: "dashboard";
|
|
35
|
+
readonly label: "Dashboard";
|
|
36
|
+
}, {
|
|
25
37
|
readonly id: "projects";
|
|
26
38
|
readonly label: "Projects";
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "conductors";
|
|
41
|
+
readonly label: "Conductors";
|
|
27
42
|
}];
|
|
28
43
|
readonly project: readonly [{
|
|
44
|
+
readonly id: "dashboard";
|
|
45
|
+
readonly label: "Dashboard";
|
|
46
|
+
}, {
|
|
29
47
|
readonly id: "tickets";
|
|
30
48
|
readonly label: "Tickets";
|
|
31
49
|
}, {
|
|
@@ -75,17 +93,6 @@ export declare function topTabBar<L extends TabLevel>(level: L, activeId: TabIdO
|
|
|
75
93
|
* not ask the compiler to correlate two independently narrowed properties.
|
|
76
94
|
*/
|
|
77
95
|
export declare function tabBarFor(location: TabLocation): string;
|
|
78
|
-
/**
|
|
79
|
-
* Which level a tab id belongs to, as a whole {@link TabLocation} — or `null`
|
|
80
|
-
* for an id no level declares.
|
|
81
|
-
*
|
|
82
|
-
* The cast is the one place the level→id pairing is asserted rather than
|
|
83
|
-
* checked, and it is sound by construction: the id was just found **in that
|
|
84
|
-
* level's own registry**, which is exactly what `TabLocation` requires.
|
|
85
|
-
* TypeScript cannot see that through the `Object.keys` loop, so it is stated
|
|
86
|
-
* here once, behind a checked lookup, instead of at every call site.
|
|
87
|
-
*/
|
|
88
|
-
export declare function locateTab(tab: TopTabId): TabLocation | null;
|
|
89
96
|
/** Where one tab sits inside the bar: `[start, end)` in display columns from the
|
|
90
97
|
* bar's own first column. */
|
|
91
98
|
export interface TopTabSpan<Id extends string = TopTabId> {
|
|
@@ -3,8 +3,9 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
3
3
|
// the top-level tabs are written. Navigation has THREE levels, and every screen
|
|
4
4
|
// shows exactly ONE tab bar: the one of its own level.
|
|
5
5
|
//
|
|
6
|
-
// root dashboard, project-picker
|
|
7
|
-
// project project, ticket-agents,
|
|
6
|
+
// root dashboard, projects, project-picker → Dashboard · Projects
|
|
7
|
+
// project dashboard, project, ticket-agents, → Dashboard · Tickets ·
|
|
8
|
+
// project-runs Ticket-Agents · Runs
|
|
8
9
|
// ticket ticket → the DETAIL tabs, drawn as the
|
|
9
10
|
// header box's meta row, so no
|
|
10
11
|
// top-level bar at all
|
|
@@ -14,11 +15,34 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
14
15
|
// (GAIA-192) — all three project-scoped, which is why they sit on the project
|
|
15
16
|
// level and not on the root one.
|
|
16
17
|
//
|
|
17
|
-
//
|
|
18
|
-
// the root
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// GAIA-256 spec D2: `dashboard` is declared on BOTH levels, on purpose. There is
|
|
19
|
+
// ONE Dashboard screen — the root one and the project one are the same screen
|
|
20
|
+
// with its project filter unset or set, and its LEVEL follows that same bit, so
|
|
21
|
+
// the bar and the filter can never disagree: they read one value.
|
|
22
|
+
//
|
|
23
|
+
// That is why tab ids are no longer globally unique, and why nothing here
|
|
24
|
+
// requires them to be. The requirement was never a property of the model — every
|
|
25
|
+
// function in this file is already level-parameterised (`topTabBar(level, id)`,
|
|
26
|
+
// `topTabSpans(level, id)`, `tabBarFor(location)`, `TabIdOf<L>`). It was a
|
|
27
|
+
// property of ONE function, the level-blind `locateTab`, which walked the levels
|
|
28
|
+
// in key order and would now resolve the project level's `dashboard` to the ROOT
|
|
29
|
+
// location. Top-tab navigation was its only caller, and it only moves within one
|
|
30
|
+
// level — the bar you Tab through or click IS the current level's bar — so it
|
|
31
|
+
// pairs the target with `here.level` directly and `locateTab` is deleted.
|
|
32
|
+
//
|
|
33
|
+
// Only tabs that are actually navigable are listed. A planned screen (`Users`)
|
|
34
|
+
// arrives with its own ticket and adds its entry here then — a tab drawn before
|
|
35
|
+
// its screen exists would advertise navigation the operator cannot take, which
|
|
36
|
+
// is the very defect GAIA-239 removes.
|
|
37
|
+
//
|
|
38
|
+
// GAIA-232 added `conductors`, and put it on the ROOT level although the note
|
|
39
|
+
// above once reserved it for the project level. A conductor is an operator
|
|
40
|
+
// concern: "is my conductor alive?" has to be answerable without first choosing
|
|
41
|
+
// a project, and one conductor commonly serves work across a checkout rather
|
|
42
|
+
// than sitting inside one project's screen. Which project it serves is a COLUMN
|
|
43
|
+
// of that screen, not a navigation level above it. Being an ordinary entry here
|
|
44
|
+
// is also what keeps it inside the derived id union and gives it GAIA-241's
|
|
45
|
+
// click spans for free.
|
|
22
46
|
//
|
|
23
47
|
// `as const satisfies` is load-bearing on both halves: `satisfies` still
|
|
24
48
|
// shape-checks every entry against `TopTab`, while `as const` keeps the literal
|
|
@@ -27,10 +51,13 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
27
51
|
// `string`, taking the compile-time check with it.
|
|
28
52
|
/** The root level: what there is before a project is chosen. */
|
|
29
53
|
export const ROOT_TABS = [
|
|
54
|
+
{ id: 'dashboard', label: 'Dashboard' },
|
|
30
55
|
{ id: 'projects', label: 'Projects' },
|
|
56
|
+
{ id: 'conductors', label: 'Conductors' },
|
|
31
57
|
];
|
|
32
58
|
/** The project level: the tabs of an opened project. */
|
|
33
59
|
export const PROJECT_TABS = [
|
|
60
|
+
{ id: 'dashboard', label: 'Dashboard' },
|
|
34
61
|
{ id: 'tickets', label: 'Tickets' },
|
|
35
62
|
{ id: 'ticket-agents', label: 'Ticket-Agents' },
|
|
36
63
|
{ id: 'runs', label: 'Runs' },
|
|
@@ -65,25 +92,6 @@ export function topTabBar(level, activeId) {
|
|
|
65
92
|
export function tabBarFor(location) {
|
|
66
93
|
return renderBar(TAB_LEVELS[location.level], location.tab);
|
|
67
94
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Which level a tab id belongs to, as a whole {@link TabLocation} — or `null`
|
|
70
|
-
* for an id no level declares.
|
|
71
|
-
*
|
|
72
|
-
* The cast is the one place the level→id pairing is asserted rather than
|
|
73
|
-
* checked, and it is sound by construction: the id was just found **in that
|
|
74
|
-
* level's own registry**, which is exactly what `TabLocation` requires.
|
|
75
|
-
* TypeScript cannot see that through the `Object.keys` loop, so it is stated
|
|
76
|
-
* here once, behind a checked lookup, instead of at every call site.
|
|
77
|
-
*/
|
|
78
|
-
export function locateTab(tab) {
|
|
79
|
-
for (const level of Object.keys(TAB_LEVELS)) {
|
|
80
|
-
const tabs = TAB_LEVELS[level];
|
|
81
|
-
if (tabs.some((t) => t.id === tab)) {
|
|
82
|
-
return { level, tab };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
95
|
const renderSpans = (tabs, activeId) => {
|
|
88
96
|
const spans = [];
|
|
89
97
|
let start = 0;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { DropshClient, JsonApiResource } from '../types.js';
|
|
2
|
+
/** A JSON:API collection document (the raw shape `client.get` returns). */
|
|
3
|
+
export interface JsonApiCollectionDoc {
|
|
4
|
+
data?: JsonApiResource[];
|
|
5
|
+
included?: JsonApiResource[];
|
|
6
|
+
/**
|
|
7
|
+
* The document-level `meta`. GAIA-305 makes every `gaia_*` collection report its
|
|
8
|
+
* total here (`GaiaResourceType::includeCount()` -> TRUE, applied by
|
|
9
|
+
* `GaiaResourceTypeRepository::countsCollections()`); see {@link Page.total}.
|
|
10
|
+
*
|
|
11
|
+
* Optional and loosely typed on purpose: a control plane that predates GAIA-305
|
|
12
|
+
* answers either with no `meta` at all or with a `meta` carrying only `omitted`
|
|
13
|
+
* — both measured this session — and neither is an error.
|
|
14
|
+
*/
|
|
15
|
+
meta?: {
|
|
16
|
+
count?: unknown;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* JSON:API's own paging links. `next` is the ONLY valid end-of-list signal, and
|
|
20
|
+
* it stays so after GAIA-305: a page can simply BE the last one, and `next` is
|
|
21
|
+
* what says whether it is. `rows.length === limit` cannot answer that — a full
|
|
22
|
+
* page is exactly as consistent with "more follows" as with "that was all", and
|
|
23
|
+
* the run collection's last page is short for the ordinary reason that
|
|
24
|
+
* collections do not divide evenly.
|
|
25
|
+
*
|
|
26
|
+
* (GAIA-305 does retire the older reason given here — that access filtering
|
|
27
|
+
* emptied pages after `page[limit]` had bounded the query, so a SHORT page said
|
|
28
|
+
* nothing at all. Its query-level access filtering makes pages full again. The
|
|
29
|
+
* conclusion is unchanged; only the argument for it is.)
|
|
30
|
+
*
|
|
31
|
+
* Optional, because the three non-paged readers (the parent picker, the projects
|
|
32
|
+
* screen, the own-conductor lookup) read one bounded bag and never page.
|
|
33
|
+
*/
|
|
34
|
+
links?: {
|
|
35
|
+
next?: unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The window a paged read ASKED the server for: the request's own `page[offset]`
|
|
40
|
+
* and `page[limit]` (GAIA-312 D3).
|
|
41
|
+
*
|
|
42
|
+
* A fact about the REQUEST, never an inference about the pages before it. Both
|
|
43
|
+
* list queries send `page[limit]=size` and `page[offset]=index * size`
|
|
44
|
+
* (`Entity/*/Data/query.ts`), and since GAIA-305 the server skips and counts the
|
|
45
|
+
* same access-filtered collection — which is what lets a screen turn an offset
|
|
46
|
+
* into a position at all.
|
|
47
|
+
*/
|
|
48
|
+
export interface PageWindow {
|
|
49
|
+
/** Rows of the collection the request told the server to skip. */
|
|
50
|
+
offset: number;
|
|
51
|
+
/** Rows the request told the server to return at most. */
|
|
52
|
+
limit: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One page of rows plus everything the screen states about it.
|
|
56
|
+
*
|
|
57
|
+
* Carried together so a `hasNext`, a page number or a total cannot go stale beside
|
|
58
|
+
* fresh rows (GAIA-292 D4/D4a).
|
|
59
|
+
*/
|
|
60
|
+
export interface Page<Row> {
|
|
61
|
+
rows: Row[];
|
|
62
|
+
hasNext: boolean;
|
|
63
|
+
/** The page these rows CAME FROM, not the one most recently requested. */
|
|
64
|
+
page: number;
|
|
65
|
+
/**
|
|
66
|
+
* The size of the filtered collection as the SERVER reported it, or null when it
|
|
67
|
+
* reported none (GAIA-292 D6e).
|
|
68
|
+
*
|
|
69
|
+
* It rides the page response's `meta.count`, so it costs no read of its own and
|
|
70
|
+
* cannot describe a different collection than the rows beside it: the server
|
|
71
|
+
* applied one query and answered both from it. That is why the earlier background
|
|
72
|
+
* walk — 28 sequential reads for the open-run collection — is gone rather than
|
|
73
|
+
* kept as a fallback.
|
|
74
|
+
*
|
|
75
|
+
* A number renders as `20 of 107 ticket(s)`; `null` renders as `20 of ?
|
|
76
|
+
* ticket(s)` (D6d), which is a visible statement that this control plane reports
|
|
77
|
+
* no total — never a number nobody established.
|
|
78
|
+
*/
|
|
79
|
+
total: number | null;
|
|
80
|
+
/**
|
|
81
|
+
* The window these rows were read with, or `null` when they no longer fill a
|
|
82
|
+
* window that positions them (GAIA-312 D3/D4).
|
|
83
|
+
*
|
|
84
|
+
* Required and explicitly nullable, exactly like {@link Page.total}: "not
|
|
85
|
+
* established" occupies the field's own place instead of being spelled by an
|
|
86
|
+
* absent key. `null` means the screen states a row COUNT; a window means it may
|
|
87
|
+
* state a RANGE, under the test in `ListScreen.countLabel`.
|
|
88
|
+
*/
|
|
89
|
+
window: PageWindow | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read one page of a collection and map it to rows.
|
|
93
|
+
*
|
|
94
|
+
* `mapRows` receives `data` AND `included` so each slice keeps its own sideload
|
|
95
|
+
* indexing — the ticket list indexes every include by id, the run list builds a
|
|
96
|
+
* ticket-only map — without this function knowing either.
|
|
97
|
+
*/
|
|
98
|
+
export declare function loadPage<Row>(client: DropshClient, path: string, params: unknown, mapRows: (data: JsonApiResource[], included: JsonApiResource[]) => Row[], page: number, window: PageWindow): Promise<Page<Row>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The ONE JSON:API collection-document shape and the ONE page read (GAIA-294).
|
|
2
|
+
//
|
|
3
|
+
// Slice-less on purpose, directly under `Entity/` beside `transport.ts`,
|
|
4
|
+
// `count.ts` and `cycle.ts`. That is the only placement every slice's `Data/` may
|
|
5
|
+
// import: `tests/boundary.test.ts` G2 forbids a data module from reaching another
|
|
6
|
+
// slice, and its `sliceOf` returns undefined for a file sitting here, so these
|
|
7
|
+
// edges are not cross-slice ones.
|
|
8
|
+
//
|
|
9
|
+
// Before this file the document was declared FIVE times — in ticket/Data/tickets,
|
|
10
|
+
// run/Data/runs, ticket/Data/parents, project/Data/projects and
|
|
11
|
+
// conductor/Data/own-conductor — and only the first two carried `links.next`. Same
|
|
12
|
+
// name, different shapes: the drift GAIA-292's "mirroring Task 3 exactly so the
|
|
13
|
+
// two lists cannot drift" was meant to prevent had already happened.
|
|
14
|
+
/**
|
|
15
|
+
* The collection total a document reports, or null when it reports none.
|
|
16
|
+
*
|
|
17
|
+
* Defensive by contract rather than by suspicion: this value is rendered to the
|
|
18
|
+
* operator as a total, so anything that is not a usable count must become the
|
|
19
|
+
* explicit unknown instead of a guess (AC 8.5). A count that is absent, not a
|
|
20
|
+
* number, not finite, or negative is no count.
|
|
21
|
+
*/
|
|
22
|
+
function readTotal(doc) {
|
|
23
|
+
const count = doc?.meta?.count;
|
|
24
|
+
if (typeof count !== 'number')
|
|
25
|
+
return null;
|
|
26
|
+
if (!Number.isFinite(count) || count < 0)
|
|
27
|
+
return null;
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read one page of a collection and map it to rows.
|
|
32
|
+
*
|
|
33
|
+
* `mapRows` receives `data` AND `included` so each slice keeps its own sideload
|
|
34
|
+
* indexing — the ticket list indexes every include by id, the run list builds a
|
|
35
|
+
* ticket-only map — without this function knowing either.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadPage(client, path, params, mapRows, page, window) {
|
|
38
|
+
const doc = (await client.get(path, params));
|
|
39
|
+
return {
|
|
40
|
+
rows: mapRows(doc?.data ?? [], doc?.included ?? []),
|
|
41
|
+
hasNext: Boolean(doc?.links?.next),
|
|
42
|
+
page,
|
|
43
|
+
total: readTotal(doc),
|
|
44
|
+
window,
|
|
45
|
+
};
|
|
46
|
+
}
|