@lotics/app-sdk 0.100.0 → 0.101.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/AGENTS.md +32 -47
- package/dist/agent_stream.d.ts +131 -0
- package/dist/ask_ai.d.ts +27 -0
- package/dist/attachments.d.ts +58 -0
- package/dist/chunk-ARV5FAU5.js +1132 -0
- package/dist/comments.d.ts +89 -0
- package/dist/error_report.d.ts +9 -0
- package/dist/folder_pick.d.ts +8 -0
- package/dist/geolocation.d.ts +42 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/{src/index.d.ts → index.d.ts} +13 -22
- package/dist/index.js +31309 -0
- package/dist/index.js.LEGAL.txt +11 -0
- package/dist/members.d.ts +32 -0
- package/dist/mock.d.ts +37 -0
- package/dist/mount.d.ts +19 -0
- package/dist/new_record.d.ts +37 -0
- package/dist/open_app.d.ts +12 -0
- package/dist/open_external.d.ts +10 -0
- package/dist/overlay.d.ts +25 -0
- package/dist/queries.d.ts +231 -0
- package/dist/recording.d.ts +47 -0
- package/dist/recording_state.d.ts +43 -0
- package/dist/rename_file.d.ts +13 -0
- package/dist/router.d.ts +10 -0
- package/dist/router.js +97 -0
- package/dist/row.d.ts +87 -0
- package/dist/rpc.d.ts +114 -0
- package/dist/select.d.ts +24 -0
- package/dist/shared_types.d.ts +8 -0
- package/dist/store.d.ts +43 -0
- package/dist/types.d.ts +36 -0
- package/dist/upload/optimize.d.ts +30 -0
- package/dist/upload/pipeline.d.ts +36 -0
- package/dist/upload/transport.d.ts +19 -0
- package/dist/url_params.d.ts +55 -0
- package/dist/use_recents.d.ts +15 -0
- package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
- package/dist/viewer.d.ts +41 -0
- package/dist/written.d.ts +77 -0
- package/docs/ai.md +74 -133
- package/docs/data_fetching.md +209 -290
- package/docs/files.md +61 -51
- package/docs/members_and_options.md +93 -63
- package/docs/mutations.md +135 -205
- package/docs/navigation_and_state.md +26 -35
- package/docs/queries.md +144 -207
- package/docs/recipes.md +21 -45
- package/docs/runtime.md +74 -137
- package/docs/security.md +8 -11
- package/docs/workflows.md +189 -174
- package/package.json +27 -28
- package/dist/src/agent_stream.d.ts +0 -200
- package/dist/src/agent_stream.js +0 -314
- package/dist/src/ask_ai.d.ts +0 -40
- package/dist/src/ask_ai.js +0 -35
- package/dist/src/attachments.d.ts +0 -68
- package/dist/src/attachments.js +0 -93
- package/dist/src/comments.d.ts +0 -127
- package/dist/src/comments.js +0 -192
- package/dist/src/download.js +0 -54
- package/dist/src/geolocation.d.ts +0 -64
- package/dist/src/geolocation.js +0 -96
- package/dist/src/hooks.d.ts +0 -781
- package/dist/src/hooks.js +0 -860
- package/dist/src/index.js +0 -34
- package/dist/src/members.d.ts +0 -105
- package/dist/src/members.js +0 -62
- package/dist/src/mock.d.ts +0 -118
- package/dist/src/mock.js +0 -124
- package/dist/src/mount.d.ts +0 -47
- package/dist/src/mount.js +0 -34
- package/dist/src/new_record.d.ts +0 -74
- package/dist/src/new_record.js +0 -117
- package/dist/src/open_app.d.ts +0 -15
- package/dist/src/open_app.js +0 -18
- package/dist/src/open_external.d.ts +0 -16
- package/dist/src/open_external.js +0 -19
- package/dist/src/recording.d.ts +0 -59
- package/dist/src/recording.js +0 -30
- package/dist/src/recording_state.d.ts +0 -59
- package/dist/src/recording_state.js +0 -94
- package/dist/src/router.d.ts +0 -17
- package/dist/src/router.js +0 -144
- package/dist/src/row.d.ts +0 -159
- package/dist/src/row.js +0 -254
- package/dist/src/rpc.d.ts +0 -207
- package/dist/src/rpc.js +0 -904
- package/dist/src/select.d.ts +0 -34
- package/dist/src/select.js +0 -40
- package/dist/src/types.d.ts +0 -115
- package/dist/src/types.js +0 -1
- package/dist/src/upload/optimize.d.ts +0 -54
- package/dist/src/upload/optimize.js +0 -207
- package/dist/src/upload/pipeline.d.ts +0 -55
- package/dist/src/upload/pipeline.js +0 -52
- package/dist/src/upload/transport.d.ts +0 -42
- package/dist/src/upload/transport.js +0 -128
- package/dist/src/url_params.d.ts +0 -93
- package/dist/src/url_params.js +0 -215
- package/dist/src/use_optimistic.d.ts +0 -27
- package/dist/src/use_optimistic.js +0 -27
- package/dist/src/use_recents.d.ts +0 -19
- package/dist/src/use_recents.js +0 -71
- package/dist/src/use_url_state.js +0 -73
- package/dist/src/viewer.d.ts +0 -26
- package/dist/src/viewer.js +0 -47
- /package/dist/{src/download.d.ts → download.d.ts} +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A member, in the one shape a `select_member` cell and the `useMembers`
|
|
3
|
+
* roster both return. `email`, `image`, `groups`, `role` and `joined` reach
|
|
4
|
+
* only an authenticated member of the org; a public visitor sees `id`, `name`
|
|
5
|
+
* and `archived`. Absent is not empty: `image: null` is "no photo", a missing
|
|
6
|
+
* `image` is "not told".
|
|
7
|
+
*/
|
|
8
|
+
export interface ResolvedMember {
|
|
9
|
+
id: string;
|
|
10
|
+
/** `null` for an id outside the org (a removed member). */
|
|
11
|
+
name: string | null;
|
|
12
|
+
email?: string | null;
|
|
13
|
+
/** Presigned; `null` without a photo. */
|
|
14
|
+
image?: string | null;
|
|
15
|
+
/** Group names — the platform's "department"; `[]` when in none. */
|
|
16
|
+
groups?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* The organization PERMISSION level, not a job title — "who is this person"
|
|
19
|
+
* is `groups`. Raw: translate it yourself.
|
|
20
|
+
*/
|
|
21
|
+
role?: "owner" | "admin" | "member";
|
|
22
|
+
/** ISO timestamp of joining the organization. */
|
|
23
|
+
joined?: string;
|
|
24
|
+
/**
|
|
25
|
+
* `true` for a member who has left; never `false`. Feed it to `inactive` on
|
|
26
|
+
* `MemberChip` / `MemberProfileCard`. The roster never returns one, since a
|
|
27
|
+
* departed member cannot be assigned.
|
|
28
|
+
*/
|
|
29
|
+
archived?: true;
|
|
30
|
+
}
|
|
31
|
+
/** `[]` for an empty or malformed cell; malformed entries are dropped. */
|
|
32
|
+
export declare function readMembers(value: unknown): ResolvedMember[];
|
package/dist/mock.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design-time fixtures, active only with BOTH `mount(<App />, { fixture })`
|
|
3
|
+
* and the `?__mock=1` URL param, so demo data in a bundle never answers real
|
|
4
|
+
* traffic. A mocked workflow never runs, so it writes nothing and nothing is
|
|
5
|
+
* drawn ahead of it; a success re-reads every mounted read.
|
|
6
|
+
* `useFileUpload` is not mocked.
|
|
7
|
+
*/
|
|
8
|
+
import type { QueryAggregate } from "./shared_types.js";
|
|
9
|
+
import type { WorkflowResult } from "./hooks.js";
|
|
10
|
+
import type { QuerySortKey } from "./queries.js";
|
|
11
|
+
import type { RecordingState } from "./recording_state.js";
|
|
12
|
+
/** A result, or a function of the inputs — return a slow promise to review the pending state. */
|
|
13
|
+
export type MockWorkflow = WorkflowResult | ((inputs: Record<string, unknown>) => WorkflowResult | Promise<WorkflowResult>);
|
|
14
|
+
export interface MockQueryCall {
|
|
15
|
+
params: Record<string, unknown>;
|
|
16
|
+
filter?: unknown;
|
|
17
|
+
sort?: readonly QuerySortKey[];
|
|
18
|
+
limit?: number;
|
|
19
|
+
/** The groups the call asks for: a fixture answers the query's rows, and they are folded into these after it. */
|
|
20
|
+
aggregate?: QueryAggregate;
|
|
21
|
+
}
|
|
22
|
+
/** Rows, or a function of the call — the only way to honour `filter`/`sort`/`limit`, which the fixture path has no engine for. */
|
|
23
|
+
export type MockQuery = Array<Record<string, unknown>> | ((call: MockQueryCall) => Array<Record<string, unknown>>);
|
|
24
|
+
export interface AppFixture {
|
|
25
|
+
queries?: Record<string, MockQuery>;
|
|
26
|
+
workflows?: Record<string, MockWorkflow>;
|
|
27
|
+
/** `useRecording(alias)` reads as available; `start`/`stop` change nothing. */
|
|
28
|
+
recordings?: Record<string, RecordingState>;
|
|
29
|
+
}
|
|
30
|
+
export declare function registerMockFixture(fixture: AppFixture | undefined): void;
|
|
31
|
+
/** The raw `?__mock=1` flag, fixture or not; false where `window.location` is absent. */
|
|
32
|
+
export declare function hasMockFlag(): boolean;
|
|
33
|
+
/** Null for an unmocked alias, which reads real data. */
|
|
34
|
+
export declare function getMockRows(alias: string, call: MockQueryCall): Array<Record<string, unknown>> | null;
|
|
35
|
+
/** Null for an unmocked alias, which executes for real. */
|
|
36
|
+
export declare function getMockWorkflow(alias: string): MockWorkflow | null;
|
|
37
|
+
export declare function getMockRecordings(): Record<string, RecordingState> | null;
|
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app's entry: `mount(<App />)` in `src/main.tsx`. A `fixture` answers
|
|
3
|
+
* reads only under `?__mock=1`:
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* mount(<App />, {
|
|
7
|
+
* fixture: {
|
|
8
|
+
* queries: { customers: MOCK_CUSTOMERS, deals: MOCK_DEALS },
|
|
9
|
+
* },
|
|
10
|
+
* });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { ReactNode } from "react";
|
|
14
|
+
import { type AppFixture } from "./mock.js";
|
|
15
|
+
export interface MountOptions {
|
|
16
|
+
/** Under `?__mock=1`; an alias it lacks reads real data. */
|
|
17
|
+
fixture?: AppFixture;
|
|
18
|
+
}
|
|
19
|
+
export declare function mount(element: ReactNode, options?: MountOptions): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A record id in the platform's shape, restated because the canonical
|
|
3
|
+
* generator is unpublished; the server validates the shape on every write.
|
|
4
|
+
* Rejection-sampled, since `% 62` over bytes would bias the first 8 characters.
|
|
5
|
+
*/
|
|
6
|
+
export declare function newRecordId(): string;
|
|
7
|
+
export interface NewRecordApi<P> {
|
|
8
|
+
/** Stable from the first render, before and after the record exists. */
|
|
9
|
+
id: string;
|
|
10
|
+
/**
|
|
11
|
+
* The first call creates, later ones update, serialised in order. Rejects as
|
|
12
|
+
* `create`/`update` did; a failed create leaves the next call to try again.
|
|
13
|
+
*/
|
|
14
|
+
save: (patch: P) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A new record named before it exists, so a surface keyed on its id never
|
|
18
|
+
* remounts on the first save. Created on the first write, not on mount, so an
|
|
19
|
+
* abandoned surface leaves nothing. The hook owns the id and the ordering: two
|
|
20
|
+
* early saves never both create, a save mid-create waits, and a failed create
|
|
21
|
+
* does not latch.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { id, save } = useNewRecord({
|
|
25
|
+
* create: (id, patch) => createCustomer({ record_id: id, ...patch }),
|
|
26
|
+
* update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
|
|
27
|
+
* onCreated: (id) => select(id), // it exists now — the list re-reads itself
|
|
28
|
+
* });
|
|
29
|
+
* <InlineText onBlur={(name) => save({ name })} />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function useNewRecord<P>(opts: {
|
|
33
|
+
create: (id: string, patch: P) => Promise<unknown>;
|
|
34
|
+
update: (id: string, patch: P) => Promise<unknown>;
|
|
35
|
+
/** Once the record exists — for routing or selecting it, not refetching (the create already re-read). */
|
|
36
|
+
onCreated?: (id: string) => void;
|
|
37
|
+
}): NewRecordApi<P>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Land on `route` — the target app's own path — inside sibling app `appId`,
|
|
3
|
+
* same tab.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { openApp } from "@lotics/app-sdk";
|
|
7
|
+
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Rejects standalone; gate on `isEmbedded()`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function openApp(appId: string, route?: string): Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL in a new tab — the sandboxed iframe drops a direct `window.open`.
|
|
3
|
+
* Only `http`/`https`; anything else rejects.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { openExternal } from "@lotics/app-sdk";
|
|
7
|
+
* await openExternal(invoiceUrl);
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare function openExternal(url: string): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type QueryAggregate } from "./shared_types.js";
|
|
2
|
+
import type { TableRecordFilters } from "./shared_types.js";
|
|
3
|
+
import { type Row } from "./written.js";
|
|
4
|
+
export interface Overlaid<T = readonly Row[]> {
|
|
5
|
+
rows: T;
|
|
6
|
+
/** A write this app made touches this read, and not all of what it changes is drawn. */
|
|
7
|
+
pending: boolean;
|
|
8
|
+
}
|
|
9
|
+
type SortKeys = readonly {
|
|
10
|
+
field_key: string;
|
|
11
|
+
order: "asc" | "desc";
|
|
12
|
+
blank_position?: "top" | "bottom";
|
|
13
|
+
}[] | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* A read's answer with this app's writes staged over it: a row read's cells, membership and new rows (`adds`:
|
|
16
|
+
* the first page only), or a declared total's counts and sums.
|
|
17
|
+
*/
|
|
18
|
+
export declare function overlayRead(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, sort: SortKeys, askedAt: number, rows: readonly Row[], adds: boolean): Overlaid;
|
|
19
|
+
/** A read's rows asked for their groups at run time, with each staged write counted into them. */
|
|
20
|
+
export declare function overlayGroups(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, aggregate: QueryAggregate, askedAt: number, rows: readonly Row[]): Overlaid;
|
|
21
|
+
/** The count of a read's rows with each staged write counted in or out. */
|
|
22
|
+
export declare function overlayCount(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, askedAt: number, total: number): Overlaid<number>;
|
|
23
|
+
/** A count per value of `by`, keyed as a row carries it, with each staged write counted in or out. */
|
|
24
|
+
export declare function overlayCounts(alias: string, params: Readonly<Record<string, unknown>>, narrowing: TableRecordFilters | undefined, by: string, askedAt: number, counts: Readonly<Record<string, number>>): Overlaid<Readonly<Record<string, number>>>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { QueryAggregate } from "./shared_types.js";
|
|
2
|
+
import type { AppQueries, AppQueryColumns } from "./types.js";
|
|
3
|
+
import type { ResolvedOption } from "./select.js";
|
|
4
|
+
/**
|
|
5
|
+
* One row of a query whose columns codegen could not read (a dynamic alias, a
|
|
6
|
+
* bare `from_table`). Values stay `unknown` — the cell readers narrow them.
|
|
7
|
+
* Only the two addressing columns are typed, because they are handed raw to a
|
|
8
|
+
* `record_id` / `table_id`; every other `__*` column has a reader that owns it.
|
|
9
|
+
*/
|
|
10
|
+
export interface QueryRow {
|
|
11
|
+
[column: string]: unknown;
|
|
12
|
+
/** The record this row came from. Absent on a grouped/aggregated row. */
|
|
13
|
+
__source_record_id?: string;
|
|
14
|
+
/** The table that record lives in. Absent on a grouped/aggregated row. */
|
|
15
|
+
__source_table_id?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The row type of alias `K`: its projected columns plus the two addressing
|
|
19
|
+
* columns, and no index signature — a column the query does not carry reads
|
|
20
|
+
* back `undefined` and renders blank, so it must fail `tsc` instead.
|
|
21
|
+
*/
|
|
22
|
+
export type RowOf<K extends string> = K extends keyof AppQueryColumns ? {
|
|
23
|
+
[C in AppQueryColumns[K] & string]: unknown;
|
|
24
|
+
} & Pick<QueryRow, "__source_record_id" | "__source_table_id"> : QueryRow;
|
|
25
|
+
/** Applied after the named query, bounded to its projected columns. */
|
|
26
|
+
export interface QuerySortKey<C extends string = string> {
|
|
27
|
+
field_key: C;
|
|
28
|
+
order: "asc" | "desc";
|
|
29
|
+
/** Where blank cells sit; `"bottom"` when omitted. */
|
|
30
|
+
blank_position?: "top" | "bottom";
|
|
31
|
+
}
|
|
32
|
+
export interface QueryFilterFieldCondition<C extends string = string> {
|
|
33
|
+
node_type: "condition";
|
|
34
|
+
field_key: C;
|
|
35
|
+
type?: string;
|
|
36
|
+
operator: string;
|
|
37
|
+
value?: unknown;
|
|
38
|
+
/** A comparison on a figure read in each row's own unit or currency: the option, by key, its value is in. */
|
|
39
|
+
unit_option?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A condition on the row's own id — no `field_key`, since no field holds it. */
|
|
42
|
+
export interface QueryFilterRecordIdCondition {
|
|
43
|
+
node_type: "condition";
|
|
44
|
+
type: "record_id";
|
|
45
|
+
operator: string;
|
|
46
|
+
value?: unknown;
|
|
47
|
+
}
|
|
48
|
+
export type QueryFilterCondition<C extends string = string> = QueryFilterFieldCondition<C> | QueryFilterRecordIdCondition;
|
|
49
|
+
export interface QueryFilterGroup<C extends string = string> {
|
|
50
|
+
node_type: "group";
|
|
51
|
+
logic: "and" | "or";
|
|
52
|
+
children: Array<QueryFilterCondition<C> | QueryFilterGroup<C>>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Runtime filter applied after the named query, bounded to its projected
|
|
56
|
+
* columns — by `C` at compile time and again on the server. Build a group with
|
|
57
|
+
* `columnFilterToConditions` (`@lotics/ui/column_filter`).
|
|
58
|
+
*/
|
|
59
|
+
export type QueryFilter<C extends string = string> = QueryFilterCondition<C> | QueryFilterGroup<C>;
|
|
60
|
+
/** A `filter`/`sort` key on alias `K`: its projected columns, else `string`. */
|
|
61
|
+
export type ColumnKeyOf<K extends string> = K extends keyof AppQueryColumns ? AppQueryColumns[K] & string : string;
|
|
62
|
+
export interface QueryOptions<C extends string = string> {
|
|
63
|
+
/**
|
|
64
|
+
* `false` sends no request and leaves `rows` empty — gate a search until
|
|
65
|
+
* there is a term, since an empty-term search matches every row. Default `true`.
|
|
66
|
+
*/
|
|
67
|
+
enabled?: boolean;
|
|
68
|
+
/** `false` skips a re-read on focus and reconnect. Default `true`. */
|
|
69
|
+
revalidateOnFocus?: boolean;
|
|
70
|
+
/** Part of the read's key; empty keeps the query's own order. */
|
|
71
|
+
sort?: QuerySortKey<C>[];
|
|
72
|
+
/** Part of the read's key. */
|
|
73
|
+
filter?: QueryFilter<C>;
|
|
74
|
+
/** The first `limit` rows only — a cap, not pages; the server clamps to its own maximum. */
|
|
75
|
+
limit?: number;
|
|
76
|
+
/** Numbered pages of `page` rows: `page`, `setPage`, `pageCount`, and the total they are counted from. */
|
|
77
|
+
page?: number;
|
|
78
|
+
/**
|
|
79
|
+
* The page in view where the screen keeps it — in its address, so a way back or a reload opens on it:
|
|
80
|
+
* `at` is read, `setPage` calls `onChange`, and a changed read never resets it. Absent, the read keeps its own.
|
|
81
|
+
*/
|
|
82
|
+
pageAt?: {
|
|
83
|
+
at: number;
|
|
84
|
+
onChange: (page: number) => void;
|
|
85
|
+
};
|
|
86
|
+
/** A feed read `more` rows at a time: `loadMore` appends the next, and a shifting set never skips a row. */
|
|
87
|
+
more?: number;
|
|
88
|
+
/**
|
|
89
|
+
* The total the rows come to — a count of the whole set, its own read, so the rows never wait for it —
|
|
90
|
+
* or with `by` a count per value of that column from the same scan. On by default with `page`.
|
|
91
|
+
*/
|
|
92
|
+
total?: boolean | {
|
|
93
|
+
by: C;
|
|
94
|
+
};
|
|
95
|
+
/** `false` reads no rows: a total alone. Default `true`. */
|
|
96
|
+
rows?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface QueryState<R> {
|
|
99
|
+
rows: R[];
|
|
100
|
+
/** The cap (`limit`, else the server's) cut the rows, so anything folded from them is under-reported. */
|
|
101
|
+
truncated: boolean;
|
|
102
|
+
/** The whole set's count, where `total` is asked; `undefined` until it answers. */
|
|
103
|
+
total: number | undefined;
|
|
104
|
+
/** Rows per value of `total.by`, keyed as a row carries it (an option's `opt_…`); a value no row holds is absent. */
|
|
105
|
+
counts: Readonly<Record<string, number>> | undefined;
|
|
106
|
+
/** Only the first read of a key with nothing to show; a re-read and a new key keep the rows on screen. */
|
|
107
|
+
loading: boolean;
|
|
108
|
+
/** Any request of this read is in flight. */
|
|
109
|
+
isValidating: boolean;
|
|
110
|
+
error: string | null;
|
|
111
|
+
/**
|
|
112
|
+
* A write this app made is in flight over these rows. They already show what it will change where that
|
|
113
|
+
* is known; the rest lands with the server's answer.
|
|
114
|
+
*/
|
|
115
|
+
pending: boolean;
|
|
116
|
+
refetch: () => void;
|
|
117
|
+
/** The page shown, from 0; always 0 without `page`. */
|
|
118
|
+
page: number;
|
|
119
|
+
/** Clamped at 0. */
|
|
120
|
+
setPage: (page: number) => void;
|
|
121
|
+
/** `undefined` without `page`, and until the total answers. */
|
|
122
|
+
pageCount: number | undefined;
|
|
123
|
+
/** A next page (`page`) or more rows (`more`) are there to read. */
|
|
124
|
+
hasMore: boolean;
|
|
125
|
+
/** Appends the next rows (`more`); nothing otherwise, or while one is being read. */
|
|
126
|
+
loadMore: () => void;
|
|
127
|
+
loadingMore: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The (params, opts) tail for alias `K`. One conditional signature, never a
|
|
131
|
+
* typed overload beside a loose one, or a rejected filter key would fall
|
|
132
|
+
* through to the loose overload and compile.
|
|
133
|
+
*/
|
|
134
|
+
type QueryArgs<K extends string, O> = K extends keyof AppQueries ? AppQueries[K] extends Record<string, never> ? [params?: Record<string, never>, opts?: O] : [params: AppQueries[K], opts?: O] : [params?: Record<string, unknown>, opts?: O];
|
|
135
|
+
/**
|
|
136
|
+
* Read a declared query by alias, filling its `{{params.x}}` holes; the server holds the AST. Its rows, a
|
|
137
|
+
* page of them (`page`), a feed of them (`more`), its total (`total`), or the total alone (`rows: false`).
|
|
138
|
+
*
|
|
139
|
+
* ```tsx
|
|
140
|
+
* const { rows, loading } = useQuery("openOrders", { status: "open" });
|
|
141
|
+
* const orders = useQuery("orders", { q }, { page: 25, sort, filter }); // orders.page, setPage, pageCount
|
|
142
|
+
* const feed = useQuery("feed", {}, { more: 30 }); // feed.loadMore, hasMore
|
|
143
|
+
* const { total, counts } = useQuery("orders", {}, { rows: false, total: { by: "stage" } });
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function useQuery<K extends string>(alias: K, ...args: QueryArgs<K, QueryOptions<ColumnKeyOf<K>>>): QueryState<RowOf<K>>;
|
|
147
|
+
/**
|
|
148
|
+
* One read of `useQueries`: a declared query, the params its holes take, a runtime narrowing of its rows, the
|
|
149
|
+
* order they are read in (empty keeps the query's own) — so a cap keeps the first rows of THAT order — its
|
|
150
|
+
* total where asked, and, asked for its groups in place of its rows, the aggregate over what the narrowing keeps.
|
|
151
|
+
*/
|
|
152
|
+
export interface QueryCall {
|
|
153
|
+
alias: string;
|
|
154
|
+
params?: Record<string, unknown>;
|
|
155
|
+
filter?: QueryFilter;
|
|
156
|
+
sort?: readonly QuerySortKey[];
|
|
157
|
+
aggregate?: QueryAggregate;
|
|
158
|
+
/** The whole set's count, its own read. */
|
|
159
|
+
total?: boolean;
|
|
160
|
+
/** `false` reads no rows: a total alone. Default `true`. */
|
|
161
|
+
rows?: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Several declared queries read together, one state per call in the calls' order — for a surface whose
|
|
165
|
+
* reads are data (a spec's blocks, a count per stage) rather than a fixed call per component. Each reads as
|
|
166
|
+
* `useQuery` with no `limit` does: the whole result up to the server's cap, `truncated` past it; a call that
|
|
167
|
+
* fails carries its own `error`, the others their rows.
|
|
168
|
+
*/
|
|
169
|
+
export declare function useQueries(calls: readonly QueryCall[], opts?: Pick<QueryOptions, "enabled" | "revalidateOnFocus">): QueryState<QueryRow>[];
|
|
170
|
+
/**
|
|
171
|
+
* Every row a declared query matches, read a page at a time to the end — for what must hold every row
|
|
172
|
+
* (a saved sheet), never for a screen, which pages. The server's rows as stored: no write is drawn over them.
|
|
173
|
+
*/
|
|
174
|
+
export declare function queryAll(alias: string, params?: Record<string, unknown>, opts?: {
|
|
175
|
+
filter?: QueryFilter;
|
|
176
|
+
sort?: readonly QuerySortKey[];
|
|
177
|
+
}): Promise<QueryRow[]>;
|
|
178
|
+
/** One select column's full option set. */
|
|
179
|
+
export interface FieldOptions {
|
|
180
|
+
/** The source field's display name. */
|
|
181
|
+
label: string;
|
|
182
|
+
/** Every option, including ones no current row holds. */
|
|
183
|
+
options: ResolvedOption[];
|
|
184
|
+
/** `undefined` for an option removed after the cell was written. */
|
|
185
|
+
byKey: (key: string) => ResolvedOption | undefined;
|
|
186
|
+
}
|
|
187
|
+
/** The units or currencies a number column is read in, row by row: the options of the select on its row naming each row's. */
|
|
188
|
+
export interface FigureUnits {
|
|
189
|
+
vocabulary: "unit" | "currency";
|
|
190
|
+
options: ResolvedOption[];
|
|
191
|
+
}
|
|
192
|
+
export interface FieldOptionsState<C extends string = string> {
|
|
193
|
+
/**
|
|
194
|
+
* Keyed by the alias's own output columns. Every key is optional: a non-select
|
|
195
|
+
* column, a UNION whose arms disagree, or a computed column has none.
|
|
196
|
+
*/
|
|
197
|
+
fields: Partial<Record<C, FieldOptions>>;
|
|
198
|
+
/**
|
|
199
|
+
* A number column read in each row's own unit or currency, keyed the same way: a
|
|
200
|
+
* condition comparing it names one of these as its `unit_option` — pass it whole as
|
|
201
|
+
* a `FilterChip` column's `units`. A figure read in one unit for every row has none.
|
|
202
|
+
*/
|
|
203
|
+
units: Partial<Record<C, FigureUnits>>;
|
|
204
|
+
loading: boolean;
|
|
205
|
+
isValidating: boolean;
|
|
206
|
+
error: string | null;
|
|
207
|
+
refetch: () => void;
|
|
208
|
+
}
|
|
209
|
+
export interface FieldOptionsOptions {
|
|
210
|
+
/** Default `true`. */
|
|
211
|
+
enabled?: boolean;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The complete option set, with colors, of each `select` column the alias
|
|
215
|
+
* projects — a cell carries only the options its record holds — and the units
|
|
216
|
+
* of each number column read in each row's own unit.
|
|
217
|
+
*
|
|
218
|
+
* ```tsx
|
|
219
|
+
* const { fields, units } = useFieldOptions("records");
|
|
220
|
+
* // populate + color a picker:
|
|
221
|
+
* <Select variant="native" options={fields.status?.options ?? []}
|
|
222
|
+
* renderOptionContent={(o) => <Status option={o} />} />
|
|
223
|
+
* // color a stored value:
|
|
224
|
+
* <Status option={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
|
|
225
|
+
* // a range on a figure each row reads in its own unit:
|
|
226
|
+
* <FilterChip column={{ key: "weight", label: "Weight", type: "number", units: units.weight }} … />
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState<ColumnKeyOf<K>>;
|
|
230
|
+
export declare function useFieldOptions(alias: string, opts?: FieldOptionsOptions): FieldOptionsState;
|
|
231
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type RecordingState } from "./recording_state.js";
|
|
2
|
+
import type { AppWorkflows } from "./types.js";
|
|
3
|
+
/** The platform fills `recording`; the app passes the rest. */
|
|
4
|
+
type ActInputsOf<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? Omit<AppWorkflows[K], "recording"> : Record<string, unknown>;
|
|
5
|
+
export interface UseRecording<I> {
|
|
6
|
+
/**
|
|
7
|
+
* False standalone, in an older host, without transcription, and until the
|
|
8
|
+
* context answers. Draw a start control only when true.
|
|
9
|
+
*/
|
|
10
|
+
available: boolean;
|
|
11
|
+
/** The latest recording through this alias. */
|
|
12
|
+
state: RecordingState;
|
|
13
|
+
/** Any of this app's recordings is live; the host records one at a time. */
|
|
14
|
+
busy: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Opens the host's capture dialog. `{ started: false }` when the member
|
|
17
|
+
* closes it; rejects with the host's reason. Once transcribed, the host runs
|
|
18
|
+
* the workflow with these inputs plus `recording`.
|
|
19
|
+
*/
|
|
20
|
+
start: {} extends I ? (inputs?: I) => Promise<{
|
|
21
|
+
started: boolean;
|
|
22
|
+
}> : (inputs: I) => Promise<{
|
|
23
|
+
started: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
/** Resolves whether or not one was live. */
|
|
26
|
+
stop: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record through the host and file it by `alias`, a workflow declaring the
|
|
30
|
+
* `recording` input, run once when the transcript is ready.
|
|
31
|
+
*
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const visit = useRecording("log_visit");
|
|
34
|
+
* if (!visit.available) return null;
|
|
35
|
+
* if (visit.state.phase === "live" && visit.state.inputs.site === siteId) {
|
|
36
|
+
* return <Button onPress={() => visit.stop()}>Stop</Button>;
|
|
37
|
+
* }
|
|
38
|
+
* return (
|
|
39
|
+
* <Button disabled={visit.busy} onPress={() => visit.start({ site: siteId })}>
|
|
40
|
+
* Record the visit
|
|
41
|
+
* </Button>
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function useRecording<K extends keyof AppWorkflows & string>(alias: K): UseRecording<ActInputsOf<K>>;
|
|
46
|
+
export declare function useRecording(alias: string): UseRecording<Record<string, unknown>>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The latest recording per workflow alias this app started. A leaf module:
|
|
3
|
+
* `rpc.ts` applies pushes, `viewer.ts` the context snapshot, both in order on
|
|
4
|
+
* one channel, so the later arrival is newer.
|
|
5
|
+
*/
|
|
6
|
+
/** The `start` inputs echoed back, so a per-row act can tell which row is recording. */
|
|
7
|
+
export type RecordingInputs = Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* `interrupted`: the tab closed mid-call, resumable from the recording bar.
|
|
10
|
+
* `processing`: transcribing. `failed`: transcription, filing or the run
|
|
11
|
+
* failed; kept, and retried from the recording bar.
|
|
12
|
+
*/
|
|
13
|
+
export type RecordingState = {
|
|
14
|
+
phase: "idle";
|
|
15
|
+
} | {
|
|
16
|
+
phase: "live";
|
|
17
|
+
elapsed_seconds: number;
|
|
18
|
+
inputs: RecordingInputs;
|
|
19
|
+
} | {
|
|
20
|
+
phase: "interrupted";
|
|
21
|
+
elapsed_seconds: number;
|
|
22
|
+
inputs: RecordingInputs;
|
|
23
|
+
} | {
|
|
24
|
+
phase: "processing";
|
|
25
|
+
inputs: RecordingInputs;
|
|
26
|
+
} | {
|
|
27
|
+
phase: "filed";
|
|
28
|
+
execution_id: string;
|
|
29
|
+
inputs: RecordingInputs;
|
|
30
|
+
} | {
|
|
31
|
+
phase: "failed";
|
|
32
|
+
message: string;
|
|
33
|
+
inputs: RecordingInputs;
|
|
34
|
+
};
|
|
35
|
+
export declare const IDLE_RECORDING: RecordingState;
|
|
36
|
+
export declare function subscribeRecordings(listener: () => void): () => void;
|
|
37
|
+
export declare function recordingStateOf(alias: string): RecordingState;
|
|
38
|
+
export declare function anyRecordingLive(): boolean;
|
|
39
|
+
export declare function applyRecordingChange(alias: string, state: RecordingState): void;
|
|
40
|
+
/** The snapshot `context` carries: every alias it omits is idle. */
|
|
41
|
+
export declare function replaceRecordings(snapshot: Record<string, RecordingState>): void;
|
|
42
|
+
export declare function readRecordingState(raw: unknown): RecordingState | null;
|
|
43
|
+
export declare function readRecordingSnapshot(raw: unknown): Record<string, RecordingState>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AppFile } from "./row.js";
|
|
2
|
+
/**
|
|
3
|
+
* A file under another name: a NEW file over the same bytes, its extension kept exactly (`"scan.pdf"` →
|
|
4
|
+
* `"Invoice 9.pdf"`, never `.html`). Nothing holds it until a save puts it in the old one's place — a files field's
|
|
5
|
+
* change taking the old id out and putting this one in, which lands it where the old one stood — so the rename is
|
|
6
|
+
* refused wherever that save is, and reaches no other field holding the old file.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const renamed = await renameFile(file.id, "Invoice 9.pdf");
|
|
10
|
+
* await save({ record_id, papers_added: [renamed.id], papers_removed: [file.id] });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function renameFile(file_id: string, filename: string): Promise<AppFile>;
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RouteObject } from "react-router";
|
|
2
|
+
/** The SDK ships no locale, so a non-English app passes its own. */
|
|
3
|
+
export interface NotFoundWords {
|
|
4
|
+
message: (path: string) => string;
|
|
5
|
+
firstScreen: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function AppRouter({ routes, notFound }: {
|
|
8
|
+
routes: RouteObject[];
|
|
9
|
+
notFound?: NotFoundWords;
|
|
10
|
+
}): import("react").JSX.Element;
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isEmbedded,
|
|
3
|
+
setUrlParams
|
|
4
|
+
} from "./chunk-ARV5FAU5.js";
|
|
5
|
+
|
|
6
|
+
// src/router.tsx
|
|
7
|
+
import { useEffect } from "react";
|
|
8
|
+
import {
|
|
9
|
+
BrowserRouter,
|
|
10
|
+
Link,
|
|
11
|
+
useLocation,
|
|
12
|
+
useRoutes
|
|
13
|
+
} from "react-router";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var LOC_KEY = "_loc";
|
|
16
|
+
var HOST_KEY = "lotics_host";
|
|
17
|
+
function screenHref(loc) {
|
|
18
|
+
const search = new URLSearchParams(loc.search);
|
|
19
|
+
search.delete(HOST_KEY);
|
|
20
|
+
const qs = search.toString();
|
|
21
|
+
return loc.pathname + (qs ? `?${qs}` : "") + loc.hash;
|
|
22
|
+
}
|
|
23
|
+
function HostScreenMirror() {
|
|
24
|
+
const location = useLocation();
|
|
25
|
+
const href = screenHref(location);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
void setUrlParams({ [LOC_KEY]: href });
|
|
28
|
+
}, [href]);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function RoutedRoutes({ routes }) {
|
|
32
|
+
return useRoutes(routes);
|
|
33
|
+
}
|
|
34
|
+
var NOT_FOUND_ROOT = {
|
|
35
|
+
display: "flex",
|
|
36
|
+
flexDirection: "column",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
gap: "var(--lotics-space-8, 8px)",
|
|
40
|
+
paddingBlock: "var(--lotics-space-48, 48px)",
|
|
41
|
+
paddingInline: "var(--lotics-space-16, 16px)",
|
|
42
|
+
textAlign: "center",
|
|
43
|
+
fontFamily: "var(--font-sans, system-ui, sans-serif)"
|
|
44
|
+
};
|
|
45
|
+
var NOT_FOUND_MESSAGE = {
|
|
46
|
+
margin: 0,
|
|
47
|
+
fontSize: "var(--lotics-text-sm, 14px)",
|
|
48
|
+
color: "var(--lotics-ink-muted, #71717a)"
|
|
49
|
+
};
|
|
50
|
+
var NOT_FOUND_LINK = {
|
|
51
|
+
fontSize: "var(--lotics-text-sm, 14px)",
|
|
52
|
+
color: "var(--lotics-accent, #2563eb)"
|
|
53
|
+
};
|
|
54
|
+
var NOT_FOUND_ENGLISH = {
|
|
55
|
+
message: (path) => `No screen at ${path}`,
|
|
56
|
+
firstScreen: "Go to the first screen"
|
|
57
|
+
};
|
|
58
|
+
function NotFoundScreen({ home, words }) {
|
|
59
|
+
const { pathname } = useLocation();
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", { style: NOT_FOUND_ROOT, children: [
|
|
61
|
+
/* @__PURE__ */ jsx("p", { style: NOT_FOUND_MESSAGE, children: words.message(pathname) }),
|
|
62
|
+
/* @__PURE__ */ jsx(Link, { to: home, style: NOT_FOUND_LINK, children: words.firstScreen })
|
|
63
|
+
] });
|
|
64
|
+
}
|
|
65
|
+
function withNotFound(routes, element) {
|
|
66
|
+
return [
|
|
67
|
+
...routes.map(
|
|
68
|
+
(route) => route.children === void 0 ? route : { ...route, children: withNotFound(route.children, element) }
|
|
69
|
+
),
|
|
70
|
+
{ path: "*", element }
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function firstScreenPath(routes) {
|
|
74
|
+
const first = routes.find((route) => route.path !== void 0 && route.path !== "*");
|
|
75
|
+
return first?.path ?? "/";
|
|
76
|
+
}
|
|
77
|
+
function AppRouter({
|
|
78
|
+
routes,
|
|
79
|
+
notFound = NOT_FOUND_ENGLISH
|
|
80
|
+
}) {
|
|
81
|
+
const embedded = isEmbedded();
|
|
82
|
+
return /* @__PURE__ */ jsxs(BrowserRouter, { children: [
|
|
83
|
+
embedded ? /* @__PURE__ */ jsx(HostScreenMirror, {}) : null,
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
RoutedRoutes,
|
|
86
|
+
{
|
|
87
|
+
routes: withNotFound(
|
|
88
|
+
routes,
|
|
89
|
+
/* @__PURE__ */ jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
AppRouter
|
|
97
|
+
};
|