@lotics/app-sdk 0.12.0 → 0.13.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/dist/src/index.d.ts +13 -5
- package/dist/src/index.js +12 -5
- package/dist/src/row.d.ts +34 -0
- package/dist/src/row.js +56 -0
- package/dist/src/use_optimistic.d.ts +22 -0
- package/dist/src/use_optimistic.js +27 -0
- package/package.json +2 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
* app at build time. Apps `import { mount, useQuery, useWorkflow } from
|
|
4
4
|
* "@lotics/app-sdk"` and ship the resulting bundle via `lotics app deploy`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* This SDK is data + RPC only — it deliberately does NOT re-export any
|
|
7
|
+
* `@lotics/ui` components, so it ships without pulling in packages/ui's
|
|
8
|
+
* React Native Web dependency tree. That is a packaging choice, NOT a
|
|
9
|
+
* limitation on apps: apps import `@lotics/ui` directly as a normal dep.
|
|
10
|
+
* The starter scaffold (`packages/sdk/src/starter_template.ts`) wires the
|
|
11
|
+
* full setup — `@lotics/ui` + react-native + react-native-web, the
|
|
12
|
+
* react-native→react-native-web Vite alias, `@lotics/ui/index.css` +
|
|
13
|
+
* `fonts.css`, and a `PortalHost` for overlays. Build dashboards by
|
|
14
|
+
* composing `@lotics/ui` components (Card, KpiCard, charts, DataGrid, …),
|
|
15
|
+
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
11
16
|
*/
|
|
12
17
|
export { mount } from "./mount.js";
|
|
13
18
|
export type { MountOptions } from "./mount.js";
|
|
@@ -19,3 +24,6 @@ export { readMembers } from "./members.js";
|
|
|
19
24
|
export type { ResolvedMember } from "./members.js";
|
|
20
25
|
export type { AppFixture } from "./mock.js";
|
|
21
26
|
export type { AppWorkflows, AppQueries } from "./types.js";
|
|
27
|
+
export { row } from "./row.js";
|
|
28
|
+
export { useOptimistic } from "./use_optimistic.js";
|
|
29
|
+
export type { OptimisticApi } from "./use_optimistic.js";
|
package/dist/src/index.js
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
* app at build time. Apps `import { mount, useQuery, useWorkflow } from
|
|
4
4
|
* "@lotics/app-sdk"` and ship the resulting bundle via `lotics app deploy`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* This SDK is data + RPC only — it deliberately does NOT re-export any
|
|
7
|
+
* `@lotics/ui` components, so it ships without pulling in packages/ui's
|
|
8
|
+
* React Native Web dependency tree. That is a packaging choice, NOT a
|
|
9
|
+
* limitation on apps: apps import `@lotics/ui` directly as a normal dep.
|
|
10
|
+
* The starter scaffold (`packages/sdk/src/starter_template.ts`) wires the
|
|
11
|
+
* full setup — `@lotics/ui` + react-native + react-native-web, the
|
|
12
|
+
* react-native→react-native-web Vite alias, `@lotics/ui/index.css` +
|
|
13
|
+
* `fonts.css`, and a `PortalHost` for overlays. Build dashboards by
|
|
14
|
+
* composing `@lotics/ui` components (Card, KpiCard, charts, DataGrid, …),
|
|
15
|
+
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
11
16
|
*/
|
|
12
17
|
export { mount } from "./mount.js";
|
|
13
18
|
export { useWorkflow, useQuery, useFileUpload } from "./hooks.js";
|
|
14
19
|
export { rpc } from "./rpc.js";
|
|
15
20
|
export { readMembers } from "./members.js";
|
|
21
|
+
export { row } from "./row.js";
|
|
22
|
+
export { useOptimistic } from "./use_optimistic.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessors that coerce raw `useQuery` row values into typed values. Each cell
|
|
3
|
+
* of a query row is `unknown`: the query layer serializes a select column as its
|
|
4
|
+
* `opt_` id (and, for some shapes, a single-element array or a `{ id }` object),
|
|
5
|
+
* a date/datetime as a `YYYY-MM-DD[THH:mm…]` string, a number/boolean as itself.
|
|
6
|
+
*
|
|
7
|
+
* These live in app-sdk — next to `useQuery`, whose serialization contract they
|
|
8
|
+
* decode — so a change to that contract updates them in one place, and so apps
|
|
9
|
+
* stop re-implementing the same coercions. They are pure (`unknown` in, value
|
|
10
|
+
* out) and never throw.
|
|
11
|
+
*/
|
|
12
|
+
/** Select → option id. Handles a bare id, a single-element array, or `{ id }`. */
|
|
13
|
+
declare function opt(v: unknown): string | null;
|
|
14
|
+
/** Text/markdown/autonumber → string (numbers stringified; everything else ""). */
|
|
15
|
+
declare function text(v: unknown): string;
|
|
16
|
+
/** Number → number (NaN/Infinity and unparseable strings → 0). */
|
|
17
|
+
declare function num(v: unknown): number;
|
|
18
|
+
/** Checkbox/boolean → boolean (accepts the string "true"). */
|
|
19
|
+
declare function bool(v: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Date/datetime field → a LOCAL-midnight Date for the stored calendar day, so
|
|
22
|
+
* calendar/gantt placement never shifts across timezones. Parses the leading
|
|
23
|
+
* `YYYY-MM-DD` of the serialized string; null if absent or unparseable. (Range
|
|
24
|
+
* fields are not handled here — they have no consumer yet.)
|
|
25
|
+
*/
|
|
26
|
+
declare function date(v: unknown): Date | null;
|
|
27
|
+
export declare const row: {
|
|
28
|
+
opt: typeof opt;
|
|
29
|
+
text: typeof text;
|
|
30
|
+
num: typeof num;
|
|
31
|
+
bool: typeof bool;
|
|
32
|
+
date: typeof date;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
package/dist/src/row.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessors that coerce raw `useQuery` row values into typed values. Each cell
|
|
3
|
+
* of a query row is `unknown`: the query layer serializes a select column as its
|
|
4
|
+
* `opt_` id (and, for some shapes, a single-element array or a `{ id }` object),
|
|
5
|
+
* a date/datetime as a `YYYY-MM-DD[THH:mm…]` string, a number/boolean as itself.
|
|
6
|
+
*
|
|
7
|
+
* These live in app-sdk — next to `useQuery`, whose serialization contract they
|
|
8
|
+
* decode — so a change to that contract updates them in one place, and so apps
|
|
9
|
+
* stop re-implementing the same coercions. They are pure (`unknown` in, value
|
|
10
|
+
* out) and never throw.
|
|
11
|
+
*/
|
|
12
|
+
/** Select → option id. Handles a bare id, a single-element array, or `{ id }`. */
|
|
13
|
+
function opt(v) {
|
|
14
|
+
if (typeof v === "string")
|
|
15
|
+
return v || null;
|
|
16
|
+
if (Array.isArray(v))
|
|
17
|
+
return v.length ? opt(v[0]) : null;
|
|
18
|
+
if (v && typeof v === "object") {
|
|
19
|
+
const id = v.id;
|
|
20
|
+
return typeof id === "string" ? id : null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Text/markdown/autonumber → string (numbers stringified; everything else ""). */
|
|
25
|
+
function text(v) {
|
|
26
|
+
if (typeof v === "string")
|
|
27
|
+
return v;
|
|
28
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
29
|
+
return String(v);
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
/** Number → number (NaN/Infinity and unparseable strings → 0). */
|
|
33
|
+
function num(v) {
|
|
34
|
+
if (typeof v === "number")
|
|
35
|
+
return Number.isFinite(v) ? v : 0;
|
|
36
|
+
if (typeof v === "string" && v.trim()) {
|
|
37
|
+
const n = Number(v);
|
|
38
|
+
return Number.isFinite(n) ? n : 0;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
/** Checkbox/boolean → boolean (accepts the string "true"). */
|
|
43
|
+
function bool(v) {
|
|
44
|
+
return v === true || v === "true";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Date/datetime field → a LOCAL-midnight Date for the stored calendar day, so
|
|
48
|
+
* calendar/gantt placement never shifts across timezones. Parses the leading
|
|
49
|
+
* `YYYY-MM-DD` of the serialized string; null if absent or unparseable. (Range
|
|
50
|
+
* fields are not handled here — they have no consumer yet.)
|
|
51
|
+
*/
|
|
52
|
+
function date(v) {
|
|
53
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(text(v));
|
|
54
|
+
return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : null;
|
|
55
|
+
}
|
|
56
|
+
export const row = { opt, text, num, bool, date };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface OptimisticApi<T> {
|
|
2
|
+
/** `base` with any pending optimistic patches applied. */
|
|
3
|
+
items: T[];
|
|
4
|
+
/**
|
|
5
|
+
* Optimistically merge `next` into the item keyed `id`, then run `persist`.
|
|
6
|
+
* On resolve → `onSettled?.()` (pass your query's `refetch`); the patch is
|
|
7
|
+
* kept (it already matches the refetched value, so no flicker). On reject →
|
|
8
|
+
* the patch is reverted.
|
|
9
|
+
*/
|
|
10
|
+
patch: (id: string, next: Partial<T>, persist: () => Promise<unknown>, opts?: {
|
|
11
|
+
onSettled?: () => void;
|
|
12
|
+
}) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Optimistic list overrides for a `useQuery` result feeding an interactive view
|
|
16
|
+
* (calendar drag, gantt resize, kanban move). Pure React state: it takes the
|
|
17
|
+
* already-mapped items + a key function + a caller-supplied `persist` thunk, so
|
|
18
|
+
* it has no coupling to any specific mutation transport (the app threads its own
|
|
19
|
+
* `useWorkflow` call + `refetch`). It lives in app-sdk as the *reconcile* leg of
|
|
20
|
+
* the read (`useQuery`) → mutate (`useWorkflow`) → reconcile loop.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useOptimistic<T>(base: T[], keyOf: (item: T) => string): OptimisticApi<T>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Optimistic list overrides for a `useQuery` result feeding an interactive view
|
|
4
|
+
* (calendar drag, gantt resize, kanban move). Pure React state: it takes the
|
|
5
|
+
* already-mapped items + a key function + a caller-supplied `persist` thunk, so
|
|
6
|
+
* it has no coupling to any specific mutation transport (the app threads its own
|
|
7
|
+
* `useWorkflow` call + `refetch`). It lives in app-sdk as the *reconcile* leg of
|
|
8
|
+
* the read (`useQuery`) → mutate (`useWorkflow`) → reconcile loop.
|
|
9
|
+
*/
|
|
10
|
+
export function useOptimistic(base, keyOf) {
|
|
11
|
+
const [overrides, setOverrides] = useState({});
|
|
12
|
+
const items = useMemo(() => base.map((item) => {
|
|
13
|
+
const o = overrides[keyOf(item)];
|
|
14
|
+
return o ? { ...item, ...o } : item;
|
|
15
|
+
}), [base, overrides, keyOf]);
|
|
16
|
+
const patch = useCallback((id, next, persist, opts) => {
|
|
17
|
+
setOverrides((m) => ({ ...m, [id]: { ...m[id], ...next } }));
|
|
18
|
+
persist().then(() => opts?.onSettled?.(), () => setOverrides((m) => {
|
|
19
|
+
if (!(id in m))
|
|
20
|
+
return m;
|
|
21
|
+
const cleared = { ...m };
|
|
22
|
+
delete cleared[id];
|
|
23
|
+
return cleared;
|
|
24
|
+
}));
|
|
25
|
+
}, []);
|
|
26
|
+
return { items, patch };
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/app-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsgo",
|
|
16
16
|
"typecheck": "tsgo --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
17
18
|
"prepublishOnly": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"peerDependencies": {
|