@lotics/app-sdk 0.33.0 → 0.34.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.
@@ -1,5 +1,6 @@
1
1
  import type { AppWorkflows, AppWorkflowResults, AppQueries } from "./types.js";
2
2
  import type { ResolvedMember } from "./members.js";
3
+ import type { ResolvedOption } from "./select.js";
3
4
  /** Fields shared by every query hook's return value. */
4
5
  interface QueryStateBase {
5
6
  /**
@@ -189,6 +190,68 @@ type QueryArgs<K extends keyof AppQueries & string, O> = AppQueries[K] extends R
189
190
  */
190
191
  export declare function useQuery<K extends keyof AppQueries & string>(alias: K, ...args: QueryArgs<K, QueryOptions>): QueryState<Record<string, unknown>>;
191
192
  export declare function useQuery(alias: string, params?: Record<string, unknown>, opts?: QueryOptions): QueryState<Record<string, unknown>>;
193
+ /** The resolved option set of one select column, plus an index for value
194
+ * rendering. The companion to a query row, for select fields. */
195
+ export interface FieldOptions {
196
+ /** The source field's display name — e.g. a picker/section label. */
197
+ label: string;
198
+ /**
199
+ * Every option of the field — `{ key, label, color }`. Includes options not
200
+ * present in any current row, so a freshly-added option appears in a picker
201
+ * without an app change, and a removed one drops out.
202
+ */
203
+ options: ResolvedOption[];
204
+ /**
205
+ * Resolve one option by key — for COLORING A STORED VALUE: pair with
206
+ * `readSelect(cell)[0]?.key`. `undefined` for an unknown key (option removed
207
+ * after the cell was written); render the cell's own label with a neutral
208
+ * badge in that case.
209
+ */
210
+ byKey: (key: string) => ResolvedOption | undefined;
211
+ }
212
+ /** Return value of `useFieldOptions`. */
213
+ export interface FieldOptionsState {
214
+ /**
215
+ * Resolved option sets keyed by the query's OUTPUT column name. A select
216
+ * column the server couldn't resolve to a source field (a UNION output, a
217
+ * computed column) is simply absent — read defensively (`fields.status?`).
218
+ */
219
+ fields: Record<string, FieldOptions>;
220
+ loading: boolean;
221
+ isValidating: boolean;
222
+ error: string | null;
223
+ /** Re-fetch — after a known field-config change (rare). */
224
+ refetch: () => void;
225
+ }
226
+ /** Options for `useFieldOptions`. */
227
+ export interface FieldOptionsOptions {
228
+ /** Defer the fetch until true — e.g. a picker that only needs options once an
229
+ * edit drawer opens. Defaults to true. */
230
+ enabled?: boolean;
231
+ }
232
+ /**
233
+ * Resolve the full option set (key, label, color) of a named query's `select`
234
+ * columns — the picker companion to `useQuery`. Where a query CELL carries only
235
+ * the options a record actually holds (key + label, no color), this returns each
236
+ * select column's COMPLETE option list with colors, straight from the field
237
+ * config — so it populates a dropdown AND colors a stored value, and a freshly
238
+ * added/removed option flows through with no app change.
239
+ *
240
+ * Addressed by the same alias you query: the option sets resolve from the named
241
+ * query's output columns, scoped exactly like running it. A column the server
242
+ * can't map to a source select field (UNION output, computed column) is absent.
243
+ *
244
+ * ```tsx
245
+ * const { fields } = useFieldOptions("records");
246
+ * // populate + color a picker:
247
+ * <Picker options={fields.status?.options ?? []}
248
+ * renderOptionContent={(o) => <OptionBadge value={o} />} />
249
+ * // color a stored value:
250
+ * <OptionBadge value={fields.status?.byKey(readSelect(r.status)[0]?.key ?? "")} />
251
+ * ```
252
+ */
253
+ export declare function useFieldOptions<K extends keyof AppQueries & string>(alias: K, opts?: FieldOptionsOptions): FieldOptionsState;
254
+ export declare function useFieldOptions(alias: string, opts?: FieldOptionsOptions): FieldOptionsState;
192
255
  /**
193
256
  * Like `useQuery` but append/load-more: the first render loads one page and
194
257
  * `loadMore()` appends the next, accumulating into `rows` (infinite scroll).
package/dist/src/hooks.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * cache survives unmount/remount. Mutation / member hooks keep their own local
16
16
  * `useState` — they have nothing to share.
17
17
  */
18
- import { useCallback, useEffect, useState } from "react";
18
+ import { useCallback, useEffect, useMemo, useState } from "react";
19
19
  import useSWR from "swr";
20
20
  import useSWRInfinite from "swr/infinite";
21
21
  import { rpc } from "./rpc.js";
@@ -76,6 +76,33 @@ export function useQuery(alias, params, opts) {
76
76
  refetch,
77
77
  };
78
78
  }
79
+ export function useFieldOptions(alias, opts) {
80
+ const enabled = opts?.enabled ?? true;
81
+ // Field config is slow-changing, so no focus/reconnect revalidation — the app
82
+ // calls `refetch()` after a known change. A null key defers the fetch.
83
+ const key = enabled ? ["app-field-options", alias] : null;
84
+ const swr = useSWR(key, () => rpc("field_options", { alias }), swrConfig(false));
85
+ const fields = useMemo(() => {
86
+ const raw = swr.data?.fields ?? {};
87
+ const out = {};
88
+ for (const [col, def] of Object.entries(raw)) {
89
+ const options = def.options ?? [];
90
+ const index = new Map(options.map((o) => [o.key, o]));
91
+ out[col] = { label: def.label, options, byKey: (k) => index.get(k) };
92
+ }
93
+ return out;
94
+ }, [swr.data]);
95
+ const refetch = useCallback(() => {
96
+ void swr.mutate();
97
+ }, [swr]);
98
+ return {
99
+ fields,
100
+ loading: swr.isLoading,
101
+ isValidating: swr.isValidating,
102
+ error: swr.error ? swr.error.message : null,
103
+ refetch,
104
+ };
105
+ }
79
106
  export function useInfiniteQuery(alias, params, opts) {
80
107
  const pageSize = opts?.pageSize ?? 30;
81
108
  const enabled = opts?.enabled ?? true;
@@ -16,8 +16,8 @@
16
16
  */
17
17
  export { mount } from "./mount.js";
18
18
  export type { MountOptions } from "./mount.js";
19
- export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFileUpload, useMembers, } from "./hooks.js";
20
- export type { UploadedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, } from "./hooks.js";
19
+ export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useMembers, } from "./hooks.js";
20
+ export type { UploadedFile, BaseQueryOptions, QueryOptions, InfiniteQueryOptions, PaginatedQueryOptions, QuerySortKey, QueryFilter, QueryFilterCondition, QueryFilterGroup, WorkflowResult, MembersOptions, FieldOptions, FieldOptionsState, FieldOptionsOptions, } from "./hooks.js";
21
21
  export { useComments, useCommentCounts } from "./comments.js";
22
22
  export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
23
23
  export { useViewer } from "./viewer.js";
package/dist/src/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
16
16
  */
17
17
  export { mount } from "./mount.js";
18
- export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFileUpload, useMembers, } from "./hooks.js";
18
+ export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useMembers, } from "./hooks.js";
19
19
  export { useComments, useCommentCounts } from "./comments.js";
20
20
  export { useViewer } from "./viewer.js";
21
21
  export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
package/dist/src/rpc.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * app → host: { id, op, payload }
19
19
  * host → app: { id, type: "result", data } | { id, type: "error", message }
20
20
  */
21
- export type RpcOp = "query" | "workflow" | "upload" | "members" | "context" | "openExternal" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
21
+ export type RpcOp = "query" | "field_options" | "workflow" | "upload" | "members" | "context" | "openExternal" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
22
22
  /**
23
23
  * The app's identity, resolved once at startup to tag PostHog events.
24
24
  * Assembled by whichever transport is active:
package/dist/src/rpc.js CHANGED
@@ -213,6 +213,8 @@ function rpcStandalone(op, payload) {
213
213
  switch (op) {
214
214
  case "query":
215
215
  return standaloneQuery(payload);
216
+ case "field_options":
217
+ return standaloneFieldOptions(payload);
216
218
  case "workflow":
217
219
  return standaloneWorkflow(payload);
218
220
  case "upload":
@@ -289,6 +291,11 @@ async function standaloneQuery(p) {
289
291
  const r = (await apiCall("POST", `/v1/apps/${app_id}/query`, { alias: p.alias, params: p.params, limit: p.limit, offset: p.offset }, { appId: app_id }));
290
292
  return { rows: r.rows ?? [] };
291
293
  }
294
+ async function standaloneFieldOptions(p) {
295
+ const { app_id } = await boot();
296
+ const r = (await apiCall("POST", `/v1/apps/${app_id}/field-options`, { alias: p.alias }, { appId: app_id }));
297
+ return { fields: r.fields ?? {} };
298
+ }
292
299
  async function standaloneWorkflow(p) {
293
300
  const { app_id } = await boot();
294
301
  return apiCall("POST", `/v1/apps/${app_id}/workflows/${encodeURIComponent(p.alias)}/execute`, { inputs: p.inputs }, { appId: app_id });
@@ -14,6 +14,14 @@ export interface ResolvedOption {
14
14
  /** Option display name. Falls back to the key when the option was deleted
15
15
  * after the cell was written — surfaces the stale state explicitly. */
16
16
  label: string;
17
+ /**
18
+ * Named palette color token (e.g. `"blue"`, `"emerald"`). Populated by
19
+ * `useFieldOptions` (which reads the field config); absent on options read
20
+ * back from a query CELL via `readSelect` — a cell carries only key + label.
21
+ * Pass the resolved option straight to `@lotics/ui`'s `OptionBadge`, which
22
+ * degrades a missing/unknown token to a neutral badge.
23
+ */
24
+ color?: string;
17
25
  }
18
26
  /**
19
27
  * Parse a `useQuery` cell value into `ResolvedOption[]`. Returns `[]` for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.33.0",
3
+ "version": "0.34.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": {