@smartmemory/wiki-components 0.3.0 → 0.5.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 +16 -3
- package/package.json +1 -1
- package/src/AreaDocs.jsx +26 -0
- package/src/Ledger.jsx +47 -0
- package/src/data-client.d.ts +20 -2
- package/src/data-client.jsx +3 -0
- package/src/index.d.ts +10 -1
- package/src/index.js +4 -1
- package/src/lib.js +2 -0
- package/src/parts/DocRow.jsx +17 -2
- package/src/parts/LedgerTimeline.jsx +20 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ npm install @smartmemory/wiki-components react react-dom date-fns lucide-react
|
|
|
26
26
|
|
|
27
27
|
## Contract
|
|
28
28
|
|
|
29
|
-
`src/data-client.d.ts` is the contract:
|
|
29
|
+
`src/data-client.d.ts` is the contract: nine calls (`identity`, `featureHub`, `decisions`, `decisionsPage`, `sourcesPage`, `docsByArea`, `ledger`, `upcomingCalls`, `search`), the endpoint and
|
|
30
30
|
request body each maps to on the wiki API, and the result types (from `wiki/contracts/query.schema.json`). The same list is
|
|
31
31
|
runtime data in `DATA_CLIENT_CALLS`; `ui/test/wiki-components.test.jsx` pins the typed file, the table and the app's client
|
|
32
32
|
to each other. `<WikiDataProvider>` refuses a client missing a method.
|
|
@@ -34,15 +34,17 @@ to each other. `<WikiDataProvider>` refuses a client missing a method.
|
|
|
34
34
|
## Use
|
|
35
35
|
|
|
36
36
|
```jsx
|
|
37
|
-
import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, wikiPath } from "@smartmemory/wiki-components";
|
|
37
|
+
import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, Ledger, wikiPath } from "@smartmemory/wiki-components";
|
|
38
38
|
|
|
39
39
|
<WikiDataProvider client={myClient}>
|
|
40
40
|
<FeatureHub featureId="CORE-1" workspace="wiki-eng" />
|
|
41
41
|
<Search query="recall" workspaces={["wiki-eng", "wiki-gtm"]}
|
|
42
|
-
onNavigate={(target) => /* {kind: "search", query?, workspace?} | {kind: "note", workspace, docId} */ go(target)}
|
|
42
|
+
onNavigate={(target) => /* {kind: "search", query?, workspace?} | {kind: "note", workspace, docId} | {kind: "area", area} */ go(target)}
|
|
43
43
|
hrefFor={(target) => `http://127.0.0.1:8791/ui${wikiPath(target)}`} />
|
|
44
44
|
<Decisions workspace="wiki-eng" />
|
|
45
45
|
<Sources workspace="wiki-eng" />
|
|
46
|
+
<AreaDocs area="search" workspace="wiki-eng" onNavigate={go} hrefFor={href} />
|
|
47
|
+
<Ledger workspace="wiki-eng" />
|
|
46
48
|
</WikiDataProvider>
|
|
47
49
|
```
|
|
48
50
|
|
|
@@ -56,6 +58,17 @@ import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, wikiPath } fr
|
|
|
56
58
|
`client.sourcesPage({workspace, cursor?, size?})` (`POST /query/sources`, `filters: {}`, `page: {size, cursor?}`), which
|
|
57
59
|
resolves `{items, total, next_cursor, generated_at}` with `SourceItem` rows. Also exported: `FreshnessBadge`,
|
|
58
60
|
`OverdueBadge`, `useNow`, `ago`, `fmtDuration`.
|
|
61
|
+
- `AreaDocs` is the paged list of the docs filed under one area (`area` prop, required), 50 a page. Its data is
|
|
62
|
+
`client.docsByArea({workspace, area, cursor?, size?})` (`POST /query/docs_by_area`, `filters: {area}`, `page: {size, cursor?}`),
|
|
63
|
+
which resolves `{items, total, next_cursor, generated_at}` with `DocItem` rows. With `onNavigate` or `hrefFor`, each doc
|
|
64
|
+
lists its areas as links, the new NavTarget `{kind: "area", area}` (`wikiPath` gives `/area/<area>`; `areaUrl` is exported).
|
|
65
|
+
- `Ledger` is the last 30 days of the workspace's ledger, newest first, 50 a page, plus an "Upcoming calls" section when
|
|
66
|
+
the host serves that feed. Its data is `client.ledger({workspace, from, to, cursor?, size?})` (`POST /query/ledger`,
|
|
67
|
+
`filters: {from, to}`, ISO instants with an offset; the sort is the host's, `ts` newest first, a screen never sends it),
|
|
68
|
+
which resolves `{items, total, next_cursor, generated_at}` with `EntryItem` rows, and `client.upcomingCalls({workspace})`
|
|
69
|
+
(`GET /feeds/upcoming_calls`), which resolves `{items, errors}`, or `null` when the host does not serve the feed (the
|
|
70
|
+
section is then left out; an empty feed still shows "No upcoming calls."). The window is 30 days back to one day ahead of
|
|
71
|
+
`now`, an optional prop (ms since the epoch) for tests, else the first render's time. Also exported: `LedgerTimeline`.
|
|
59
72
|
- `workspace` / `workspaces` are optional: without them a screen calls `client.identity()` and uses the caller's grants
|
|
60
73
|
(FeatureHub the first one).
|
|
61
74
|
- `onNavigate(target)` receives a plain left click on a link and the search box's submit. `hrefFor(target)` is each link's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/wiki-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The wiki's feature hub and search screens as router-free React components fed by an injected DataClient (WIKI-UI-COMPONENTS-1). Ships JSX source for a bundler host (Vite).",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Regression Analytics LLC",
|
package/src/AreaDocs.jsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useDataClient, useWorkspaces } from "./data-client";
|
|
3
|
+
import { usePagedCall } from "./paged";
|
|
4
|
+
import ListPage from "./parts/ListPage";
|
|
5
|
+
import PageHeader from "./parts/PageHeader";
|
|
6
|
+
import DocRow from "./parts/DocRow";
|
|
7
|
+
import Loading from "./parts/Loading";
|
|
8
|
+
import Pager from "./parts/Pager";
|
|
9
|
+
import ErrorBox from "./parts/ErrorBox";
|
|
10
|
+
|
|
11
|
+
/** The docs filed under one area: `area` of `workspace` (default: the first of identity().workspaces), 50 a page with a
|
|
12
|
+
* pager. Each doc lists its areas as links: `onNavigate({kind: "area", area})` follows one, `hrefFor(target)` is its href.
|
|
13
|
+
* Data: DataClient docsByArea. */
|
|
14
|
+
export default function AreaDocs({ area, workspace: given, onNavigate, hrefFor }) {
|
|
15
|
+
const client = useDataClient();
|
|
16
|
+
const ws = useWorkspaces(given ? [given] : undefined);
|
|
17
|
+
const workspace = given || ws.workspaces?.[0];
|
|
18
|
+
const p = usePagedCall(["docs_by_area", workspace, area], (page) => client.docsByArea({ workspace, area, ...page }), { enabled: !!workspace && !!area });
|
|
19
|
+
const error = p.error || ws.error;
|
|
20
|
+
return (
|
|
21
|
+
<ListPage header={<PageHeader eyebrow="Area" title={area}>{p.total != null && `${p.total} docs`}</PageHeader>} foot={<Pager p={p} />}>
|
|
22
|
+
<ErrorBox error={error} />
|
|
23
|
+
{p.isLoading || ws.isLoading ? <Loading /> : !error && p.items.length === 0 ? <p className="text-sm text-neutral-400">No docs in this area yet.</p> : p.items.map((d) => <DocRow key={d.doc_id} doc={d} onNavigate={onNavigate} hrefFor={hrefFor} />)}
|
|
24
|
+
</ListPage>
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/Ledger.jsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { useClientCall, useDataClient, useWorkspaces } from "./data-client";
|
|
3
|
+
import { usePagedCall } from "./paged";
|
|
4
|
+
import ListPage from "./parts/ListPage";
|
|
5
|
+
import PageHeader from "./parts/PageHeader";
|
|
6
|
+
import LedgerTimeline from "./parts/LedgerTimeline";
|
|
7
|
+
import EntryRows from "./parts/EntryRows";
|
|
8
|
+
import Section from "./parts/Section";
|
|
9
|
+
import Loading from "./parts/Loading";
|
|
10
|
+
import Pager from "./parts/Pager";
|
|
11
|
+
import ErrorBox from "./parts/ErrorBox";
|
|
12
|
+
|
|
13
|
+
const DAY_MS = 86_400_000;
|
|
14
|
+
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d+Z$/, "Z");
|
|
15
|
+
|
|
16
|
+
/** The ledger timeline: the last 30 days of `workspace` (default: the first of identity().workspaces), newest first, 50 a
|
|
17
|
+
* page with a pager, and an "Upcoming calls" section when the host serves that feed. The window runs from 30 days before
|
|
18
|
+
* `now` to one day after it (ms since the epoch; default: the time of the first render, kept for the life of the screen so
|
|
19
|
+
* the query key stays stable). `onNavigate` / `hrefFor` are accepted for the host contract shared by the screens; the
|
|
20
|
+
* timeline has no links yet. Data: DataClient ledger (one call per page) and upcomingCalls (one call). */
|
|
21
|
+
export default function Ledger({ workspace: given, now }) {
|
|
22
|
+
const client = useDataClient();
|
|
23
|
+
const ws = useWorkspaces(given ? [given] : undefined);
|
|
24
|
+
const workspace = given || ws.workspaces?.[0];
|
|
25
|
+
const fixedNow = useMemo(() => Date.now(), []);
|
|
26
|
+
const at = now ?? fixedNow;
|
|
27
|
+
const range = useMemo(() => ({ from: iso(at - 30 * DAY_MS), to: iso(at + DAY_MS) }), [at]);
|
|
28
|
+
const p = usePagedCall(["ledger", workspace, range], (page) => client.ledger({ workspace, ...range, ...page }), { enabled: !!workspace });
|
|
29
|
+
const calls = useClientCall(["upcoming_calls", workspace], () => client.upcomingCalls({ workspace }), { enabled: !!workspace });
|
|
30
|
+
const error = p.error || ws.error;
|
|
31
|
+
return (
|
|
32
|
+
<ListPage header={<PageHeader title="Ledger">{p.total != null && `${p.total.toLocaleString()} entries in the last 30 days, newest first`}</PageHeader>} foot={<Pager p={p} />}>
|
|
33
|
+
<ErrorBox error={error} />
|
|
34
|
+
{p.isLoading || ws.isLoading ? <Loading /> : <LedgerTimeline entries={p.items} />}
|
|
35
|
+
{/* the calendar feed is served only on a host that wires it: elsewhere (null) there is no calls section */}
|
|
36
|
+
{!calls.isLoading && calls.data !== null && (calls.data || calls.error) && (
|
|
37
|
+
<Section title="Upcoming calls" className="mt-12">
|
|
38
|
+
{(calls.data?.errors || []).map((e) => (
|
|
39
|
+
<p key={e.code} role="status" className="mb-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">Calls are incomplete: {e.message} ({e.code})</p>
|
|
40
|
+
))}
|
|
41
|
+
<ErrorBox error={calls.error} />
|
|
42
|
+
{calls.data && <EntryRows entries={calls.data.items} empty="No upcoming calls." />}
|
|
43
|
+
</Section>
|
|
44
|
+
)}
|
|
45
|
+
</ListPage>
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/data-client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// WIKI-UI-COMPONENTS-1: the DataClient contract. Every call the FeatureHub, Search, Decisions and
|
|
1
|
+
// WIKI-UI-COMPONENTS-1: the DataClient contract. Every call the FeatureHub, Search, Decisions, Sources, AreaDocs and Ledger screens make, and nothing else.
|
|
2
2
|
// A host (the wiki's /ui/ app, Compose's wiki tab) passes one object with these async methods to <WikiDataProvider>.
|
|
3
3
|
// The components never fetch: each call below is the whole of their I/O.
|
|
4
4
|
//
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// decisions POST /query/decisions {query: "decisions", workspace, filters: {feature_id}, page: {size: 50}}
|
|
12
12
|
// decisionsPage POST /query/decisions {query: "decisions", workspace, filters: {}, page: {size: 50, cursor?}}
|
|
13
13
|
// sourcesPage POST /query/sources {query: "sources", workspace, filters: {}, page: {size: 50, cursor?}}
|
|
14
|
+
// docsByArea POST /query/docs_by_area {query: "docs_by_area", workspace, filters: {area}, page: {size: 50, cursor?}}
|
|
15
|
+
// ledger POST /query/ledger {query: "ledger", workspace, filters: {from, to}, page: {size: 50, cursor?}} (sort is the host's: ts, newest first)
|
|
16
|
+
// upcomingCalls GET /feeds/upcoming_calls?workspace= (none) -> data_envelope; an absent feed (the wiki answers 404 not_found) resolves to null
|
|
14
17
|
// search POST /query/search {query: "search", workspace, filters: {q}, page: {}}
|
|
15
18
|
//
|
|
16
19
|
// The same table is runtime data in data-client.jsx (DATA_CLIENT_CALLS); a test pins the two to each other.
|
|
@@ -104,6 +107,12 @@ export interface SourceItem {
|
|
|
104
107
|
overdue: boolean | null;
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
/** The upcoming_calls feed (component.schema.json data_envelope): `items` are entries; `errors` say what a partial answer is missing. */
|
|
111
|
+
export interface UpcomingCalls {
|
|
112
|
+
items: EntryItem[];
|
|
113
|
+
errors: { code: string; message?: string }[];
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
/** x-named-queries.feature_hub.result: the feature's docs plus the feature itself (null when it is not on the roadmap). */
|
|
108
117
|
export interface FeatureHubResult extends QueryResult<DocItem> {
|
|
109
118
|
feature: FeatureItem | null;
|
|
@@ -128,6 +137,14 @@ export interface DataClient {
|
|
|
128
137
|
/** POST /query/sources, filters {}, page {size (default 50), cursor?}: one page of the workspace's ingestion sources.
|
|
129
138
|
* `cursor` is the previous page's `next_cursor`, opaque. */
|
|
130
139
|
sourcesPage(args: { workspace: Workspace; cursor?: string; size?: number }): Promise<QueryResult<SourceItem>>;
|
|
140
|
+
/** POST /query/docs_by_area, filters {area} (required, 1 to 200 characters), page {size (default 50), cursor?}: one page of the live docs
|
|
141
|
+
* filed under `area`. `cursor` is the previous page's `next_cursor`, opaque. */
|
|
142
|
+
docsByArea(args: { workspace: Workspace; area: string; cursor?: string; size?: number }): Promise<QueryResult<DocItem>>;
|
|
143
|
+
/** POST /query/ledger, filters {from, to} (both required: ISO instants with an offset, `from` <= `to`, at most 366 days apart), page {size (default 50), cursor?}:
|
|
144
|
+
* one page of the ledger entries in the window, newest first (the sort is fixed by the host, never sent by a screen). `cursor` is the previous page's `next_cursor`, opaque. */
|
|
145
|
+
ledger(args: { workspace: Workspace; from: string; to: string; cursor?: string; size?: number }): Promise<QueryResult<EntryItem>>;
|
|
146
|
+
/** GET /feeds/upcoming_calls: the calendar feed's items, or null when this host does not serve the feed (not an error, not an empty list). */
|
|
147
|
+
upcomingCalls(args: { workspace: Workspace }): Promise<UpcomingCalls | null>;
|
|
131
148
|
/** POST /query/search, filters {q} (1 to 256 characters), page {}. One call per workspace searched. */
|
|
132
149
|
search(args: { workspace: Workspace; q: string }): Promise<SearchResult>;
|
|
133
150
|
}
|
|
@@ -136,4 +153,5 @@ export interface DataClient {
|
|
|
136
153
|
* (the /ui/ app: wikiPath()). */
|
|
137
154
|
export type NavTarget =
|
|
138
155
|
| { kind: "search"; query?: string; workspace?: Workspace }
|
|
139
|
-
| { kind: "note"; workspace: Workspace; docId: string }
|
|
156
|
+
| { kind: "note"; workspace: Workspace; docId: string }
|
|
157
|
+
| { kind: "area"; area: string };
|
package/src/data-client.jsx
CHANGED
|
@@ -8,6 +8,9 @@ export const DATA_CLIENT_CALLS = [
|
|
|
8
8
|
{ method: "decisions", endpoint: "POST /query/decisions", query: "decisions", page: { size: 50 } },
|
|
9
9
|
{ method: "decisionsPage", endpoint: "POST /query/decisions", query: "decisions", page: { size: 50 } },
|
|
10
10
|
{ method: "sourcesPage", endpoint: "POST /query/sources", query: "sources", page: { size: 50 } },
|
|
11
|
+
{ method: "docsByArea", endpoint: "POST /query/docs_by_area", query: "docs_by_area", page: { size: 50 } },
|
|
12
|
+
{ method: "ledger", endpoint: "POST /query/ledger", query: "ledger", page: { size: 50 } },
|
|
13
|
+
{ method: "upcomingCalls", endpoint: "GET /feeds/upcoming_calls" },
|
|
11
14
|
{ method: "search", endpoint: "POST /query/search", query: "search", page: {} },
|
|
12
15
|
];
|
|
13
16
|
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Types for @smartmemory/wiki-components. The DataClient contract and NavTarget: data-client.d.ts.
|
|
2
2
|
import type { ReactElement, ReactNode } from "react";
|
|
3
|
-
import type { DataClient, NavTarget, QueryResult, Workspace } from "./data-client";
|
|
3
|
+
import type { DataClient, EntryItem, NavTarget, QueryResult, Workspace } from "./data-client";
|
|
4
4
|
|
|
5
5
|
export * from "./data-client";
|
|
6
6
|
|
|
@@ -28,6 +28,13 @@ export function Decisions(props: { workspace?: Workspace | null } & NavProps): R
|
|
|
28
28
|
/** Every ingestion source of `workspace` (default: the first of identity().workspaces), 50 a page with a pager. */
|
|
29
29
|
export function Sources(props: { workspace?: Workspace | null } & NavProps): ReactElement;
|
|
30
30
|
|
|
31
|
+
/** The docs filed under `area` of `workspace` (default: the first of identity().workspaces), 50 a page with a pager. Each doc's other areas link to their own AreaDocs through `onNavigate({kind: "area", area})`. */
|
|
32
|
+
export function AreaDocs(props: { area: string; workspace?: Workspace | null } & NavProps): ReactElement;
|
|
33
|
+
|
|
34
|
+
/** The last 30 days of `workspace` (default: the first of identity().workspaces), newest first, 50 a page with a pager, plus an "Upcoming calls" section when the host serves that feed. `now` (ms since the epoch) fixes the window's end for tests; default: the first render's time. */
|
|
35
|
+
export function Ledger(props: { workspace?: Workspace | null; now?: number } & NavProps): ReactElement;
|
|
36
|
+
export function LedgerTimeline(props: { entries: EntryItem[] }): ReactElement;
|
|
37
|
+
|
|
31
38
|
/** One DataClient call paged by cursor. `run({cursor?, size})` fetches a page; a new `key` restarts at page one. */
|
|
32
39
|
export function usePagedCall<T>(
|
|
33
40
|
key: unknown,
|
|
@@ -46,3 +53,5 @@ export function Pager(props: { p: { total?: number; index: number; size: number;
|
|
|
46
53
|
/** A NavTarget as a path of the wiki's /ui/ app (relative to its basename, e.g. "/search?q=x"). */
|
|
47
54
|
export function wikiPath(target: NavTarget): string;
|
|
48
55
|
export function wsName(workspace: Workspace): string;
|
|
56
|
+
/** The path of an area's docs page in the wiki's /ui/ app. */
|
|
57
|
+
export function areaUrl(area: string): string;
|
package/src/index.js
CHANGED
|
@@ -4,9 +4,11 @@ export { default as FeatureHub, prefixOnly } from "./FeatureHub";
|
|
|
4
4
|
export { default as Search } from "./Search";
|
|
5
5
|
export { default as Decisions } from "./Decisions";
|
|
6
6
|
export { default as Sources } from "./Sources";
|
|
7
|
+
export { default as AreaDocs } from "./AreaDocs";
|
|
8
|
+
export { default as Ledger } from "./Ledger";
|
|
7
9
|
export { usePagedCall } from "./paged";
|
|
8
10
|
export { WikiDataProvider, useDataClient, useClientCall, DATA_CLIENT_CALLS } from "./data-client";
|
|
9
|
-
export { wikiPath, wsName, wsUrl, noteUrl, toDate, fmtDate, ago, fmtDuration, summaryOf } from "./lib";
|
|
11
|
+
export { wikiPath, wsName, wsUrl, noteUrl, areaUrl, toDate, fmtDate, ago, fmtDuration, summaryOf } from "./lib";
|
|
10
12
|
export { default as useNow } from "./useNow";
|
|
11
13
|
// The parts the screens are built from; the /ui/ app's other pages use them too (ui/src/components/wiki/* re-export them).
|
|
12
14
|
export { default as ErrorBox } from "./parts/ErrorBox";
|
|
@@ -24,5 +26,6 @@ export { default as SearchBox } from "./parts/SearchBox";
|
|
|
24
26
|
export { default as NavLink } from "./parts/NavLink";
|
|
25
27
|
export { default as Table } from "./parts/Table";
|
|
26
28
|
export { default as Pager } from "./parts/Pager";
|
|
29
|
+
export { default as LedgerTimeline } from "./parts/LedgerTimeline";
|
|
27
30
|
export { default as FreshnessBadge } from "./parts/FreshnessBadge";
|
|
28
31
|
export { default as OverdueBadge } from "./parts/OverdueBadge";
|
package/src/lib.js
CHANGED
|
@@ -10,6 +10,7 @@ export const wsName = (ws) => {
|
|
|
10
10
|
return WS_NAMES[tail] || tail.split(/[-_]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ") || "Workspace";
|
|
11
11
|
};
|
|
12
12
|
export const wsUrl = (ws) => `/w/${encodeURIComponent(ws)}`;
|
|
13
|
+
export const areaUrl = (a) => `/area/${encodeURIComponent(a)}`;
|
|
13
14
|
export const noteUrl = (ws, id) => `${wsUrl(ws)}/note?id=${encodeURIComponent(id)}`;
|
|
14
15
|
|
|
15
16
|
export const toDate = (d) => (d ? new Date(d.length <= 10 || /([zZ]|[+-]\d\d:?\d\d)$/.test(d) ? d : d + "Z") : null);
|
|
@@ -33,6 +34,7 @@ export const summaryOf = (feature) => (typeof feature?.summary === "string" && f
|
|
|
33
34
|
/** A NavTarget (data-client.d.ts) as a route of the wiki's own /ui/ app, relative to its basename. */
|
|
34
35
|
export function wikiPath(target) {
|
|
35
36
|
if (target.kind === "note") return noteUrl(target.workspace, target.docId);
|
|
37
|
+
if (target.kind === "area") return areaUrl(target.area);
|
|
36
38
|
if (target.kind === "search") {
|
|
37
39
|
const p = new URLSearchParams({ ...(target.query ? { q: target.query } : {}), ...(target.workspace ? { workspace: target.workspace } : {}) });
|
|
38
40
|
return `/search?${p}`;
|
package/src/parts/DocRow.jsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import TypeBadge from "./TypeBadge";
|
|
3
|
+
import NavLink from "./NavLink";
|
|
3
4
|
import { fmtDate } from "../lib";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/** One doc. With `onNavigate` or `hrefFor` (a screen that links, e.g. AreaDocs) the doc's areas are listed under the row,
|
|
7
|
+
* each a NavTarget {kind: "area"}; without them the row is exactly as before. */
|
|
8
|
+
export default function DocRow({ doc, right, onNavigate, hrefFor }) {
|
|
9
|
+
const links = (onNavigate || hrefFor) && doc.areas?.length > 0;
|
|
10
|
+
const row = (
|
|
7
11
|
<div className="grid items-center gap-3 rounded-md px-3 py-2 hover:bg-neutral-50 grid-cols-[6.5rem_1fr_auto] sm:grid-cols-[6.5rem_1fr_16rem_6.5rem]">
|
|
8
12
|
<TypeBadge type={doc.kind} />
|
|
9
13
|
<span className="truncate font-medium text-neutral-900">{doc.title}</span>
|
|
@@ -12,4 +16,15 @@ export default function DocRow({ doc, right }) {
|
|
|
12
16
|
{right}
|
|
13
17
|
</div>
|
|
14
18
|
);
|
|
19
|
+
if (!links) return row;
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
{row}
|
|
23
|
+
<div className="flex flex-wrap gap-1.5 px-3 pb-2 pl-[7.75rem]" data-testid="area-links">
|
|
24
|
+
{doc.areas.map((a) => (
|
|
25
|
+
<NavLink key={a} target={{ kind: "area", area: a }} hrefFor={hrefFor} onNavigate={onNavigate} className="rounded-full border border-neutral-200 px-2 py-0.5 text-[11px] text-neutral-600 hover:border-blue-500 hover:text-neutral-900">{a}</NavLink>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
15
30
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtDate } from "../lib";
|
|
3
|
+
|
|
4
|
+
export default function LedgerTimeline({ entries }) {
|
|
5
|
+
if (!entries.length) return <p className="px-3 text-sm text-neutral-400">No ledger entries.</p>;
|
|
6
|
+
return (
|
|
7
|
+
<ol className="relative ml-2 border-l border-neutral-200">
|
|
8
|
+
{entries.map((e, i) => (
|
|
9
|
+
<li key={e.entry_id} className="relative pb-4 pl-5">
|
|
10
|
+
<span className="absolute -left-[4.5px] top-1.5 h-2 w-2 rounded-full bg-neutral-300" />
|
|
11
|
+
{(i === 0 || entries[i - 1].ts.slice(0, 10) !== e.ts.slice(0, 10)) && <div className="mb-1 text-xs font-semibold text-neutral-500">{fmtDate(e.ts)}</div>}
|
|
12
|
+
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
13
|
+
<span className="font-mono text-[11px] text-neutral-400">{e.ts.slice(11, 16)}</span>
|
|
14
|
+
<span className="text-[13px] text-neutral-800">{e.text}</span>
|
|
15
|
+
</div>
|
|
16
|
+
</li>
|
|
17
|
+
))}
|
|
18
|
+
</ol>
|
|
19
|
+
);
|
|
20
|
+
}
|