@lotics/app-sdk 0.32.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.
- package/dist/src/download.d.ts +1 -0
- package/dist/src/download.js +54 -0
- package/dist/src/hooks.d.ts +64 -1
- package/dist/src/hooks.js +28 -1
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.js +3 -2
- package/dist/src/row.d.ts +11 -0
- package/dist/src/row.js +16 -1
- package/dist/src/rpc.d.ts +1 -1
- package/dist/src/rpc.js +7 -0
- package/dist/src/select.d.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function downloadFile(filename: string, data: Uint8Array | Blob | string, mimeType?: string): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save bytes the app generated in the browser to the visitor's device.
|
|
3
|
+
*
|
|
4
|
+
* This is the client-side counterpart to `openExternal`. The platform's other
|
|
5
|
+
* download path is server-side: a workflow generates a file, it comes back in
|
|
6
|
+
* `WorkflowResult.files[]` with a `.url`, and the app opens that URL with
|
|
7
|
+
* `openExternal`. But data an app builds *in the browser* (an .xlsx exported
|
|
8
|
+
* from a query result, a CSV, a generated PDF) never gets a server URL — there
|
|
9
|
+
* was no primitive to save it. This is that primitive.
|
|
10
|
+
*
|
|
11
|
+
* It is pure DOM, not an RPC: the embed iframe is sandboxed with
|
|
12
|
+
* `allow-downloads` (see `app_iframe_host`), so a same-origin blob download
|
|
13
|
+
* triggered from a user gesture is permitted without host mediation. In
|
|
14
|
+
* standalone (public) mode the app is a normal top-level page, where downloads
|
|
15
|
+
* always work. Call it synchronously from the click handler that produced the
|
|
16
|
+
* bytes so the browser attributes the download to the user gesture.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { downloadFile } from "@lotics/app-sdk";
|
|
20
|
+
* import { buildDataWorkbook, exportWorkbook } from "@lotics/xlsx";
|
|
21
|
+
*
|
|
22
|
+
* const wb = buildDataWorkbook({ columns, rows });
|
|
23
|
+
* downloadFile("report.xlsx", exportWorkbook(wb), XLSX_MIME);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_MIME = "application/octet-stream";
|
|
27
|
+
function toBlob(data, mimeType) {
|
|
28
|
+
if (data instanceof Blob)
|
|
29
|
+
return data;
|
|
30
|
+
if (typeof data === "string")
|
|
31
|
+
return new Blob([data], { type: mimeType });
|
|
32
|
+
// Re-wrap the bytes: the DOM lib types a generic `Uint8Array<ArrayBufferLike>`
|
|
33
|
+
// as incompatible with `BlobPart`, and `new Uint8Array(data)` narrows it to a
|
|
34
|
+
// fresh `ArrayBuffer`-backed view (the pattern `save_workbook_version` uses).
|
|
35
|
+
return new Blob([new Uint8Array(data)], { type: mimeType });
|
|
36
|
+
}
|
|
37
|
+
export function downloadFile(filename, data, mimeType) {
|
|
38
|
+
const url = URL.createObjectURL(toBlob(data, mimeType ?? DEFAULT_MIME));
|
|
39
|
+
try {
|
|
40
|
+
const a = document.createElement("a");
|
|
41
|
+
a.href = url;
|
|
42
|
+
a.download = filename;
|
|
43
|
+
a.rel = "noopener";
|
|
44
|
+
a.style.display = "none";
|
|
45
|
+
document.body.appendChild(a);
|
|
46
|
+
a.click();
|
|
47
|
+
a.remove();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
// Revoke after the current task so the navigation to the blob URL has
|
|
51
|
+
// started — revoking synchronously can cancel the download in some browsers.
|
|
52
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -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).
|
|
@@ -202,7 +265,7 @@ export declare function useInfiniteQuery<K extends keyof AppQueries & string>(al
|
|
|
202
265
|
export declare function useInfiniteQuery(alias: string, params?: Record<string, unknown>, opts?: InfiniteQueryOptions): InfiniteQueryState<Record<string, unknown>>;
|
|
203
266
|
/**
|
|
204
267
|
* Page-model query with a total — the data hook behind a numbered, jumpable
|
|
205
|
-
* table (
|
|
268
|
+
* table (pairs with `@lotics/ui/pagination`). It owns the page
|
|
206
269
|
* cursor and fetches two things: the current page of rows, and a `count` over
|
|
207
270
|
* the filtered set (keyed independently of page + sort, so paging and
|
|
208
271
|
* re-sorting never recount). The `(params, filter)` tuple is the result-set
|
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;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* full setup — `@lotics/ui` + react-native + react-native-web, the
|
|
12
12
|
* react-native→react-native-web Vite alias, `@lotics/ui/index.css` +
|
|
13
13
|
* `fonts.css`, and a `PortalHost` for overlays. Build dashboards by
|
|
14
|
-
* composing `@lotics/ui` components (Card, KpiCard, charts,
|
|
14
|
+
* composing `@lotics/ui` components (Card, KpiCard, charts, Table, …),
|
|
15
15
|
* not raw HTML/CSS. See `docs/apps.md` → "Styling & components".
|
|
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";
|
|
@@ -26,6 +26,7 @@ export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "
|
|
|
26
26
|
export { rpc } from "./rpc.js";
|
|
27
27
|
export type { RpcOp } from "./rpc.js";
|
|
28
28
|
export { openExternal } from "./open_external.js";
|
|
29
|
+
export { downloadFile } from "./download.js";
|
|
29
30
|
export { readMembers } from "./members.js";
|
|
30
31
|
export type { ResolvedMember } from "./members.js";
|
|
31
32
|
export { readSelect } from "./select.js";
|
package/dist/src/index.js
CHANGED
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
* full setup — `@lotics/ui` + react-native + react-native-web, the
|
|
12
12
|
* react-native→react-native-web Vite alias, `@lotics/ui/index.css` +
|
|
13
13
|
* `fonts.css`, and a `PortalHost` for overlays. Build dashboards by
|
|
14
|
-
* composing `@lotics/ui` components (Card, KpiCard, charts,
|
|
14
|
+
* composing `@lotics/ui` components (Card, KpiCard, charts, Table, …),
|
|
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";
|
|
22
22
|
export { rpc } from "./rpc.js";
|
|
23
23
|
export { openExternal } from "./open_external.js";
|
|
24
|
+
export { downloadFile } from "./download.js";
|
|
24
25
|
export { readMembers } from "./members.js";
|
|
25
26
|
export { readSelect } from "./select.js";
|
|
26
27
|
export { row, readLinks, readFiles, readLocked } from "./row.js";
|
package/dist/src/row.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ declare function bool(v: unknown): boolean;
|
|
|
28
28
|
* fields are not handled here — they have no consumer yet.)
|
|
29
29
|
*/
|
|
30
30
|
declare function date(v: unknown): Date | null;
|
|
31
|
+
/**
|
|
32
|
+
* Date/datetime field → a LOCAL Date that KEEPS the stored wall-clock time, so
|
|
33
|
+
* `getHours()` / `toLocaleTimeString()` render the time as written. The stored
|
|
34
|
+
* value is a timezone-less workspace wall-clock (see the serialization note
|
|
35
|
+
* above), so it is read verbatim — no UTC conversion. Use this when the time
|
|
36
|
+
* matters (a check-in time, an appointment); `date` keeps only the calendar day.
|
|
37
|
+
* Parses `YYYY-MM-DD` with an optional `T`-or-space `HH:mm[:ss]`; a missing time
|
|
38
|
+
* is midnight. Null if absent or unparseable.
|
|
39
|
+
*/
|
|
40
|
+
declare function datetime(v: unknown): Date | null;
|
|
31
41
|
/** A linked record cell entry — the target record's id + its display text. */
|
|
32
42
|
export interface ResolvedLink {
|
|
33
43
|
/** The linked record's id (e.g. "rec_…"). Use to correlate/filter. */
|
|
@@ -80,6 +90,7 @@ export declare const row: {
|
|
|
80
90
|
num: typeof num;
|
|
81
91
|
bool: typeof bool;
|
|
82
92
|
date: typeof date;
|
|
93
|
+
datetime: typeof datetime;
|
|
83
94
|
link: typeof link;
|
|
84
95
|
};
|
|
85
96
|
export {};
|
package/dist/src/row.js
CHANGED
|
@@ -60,6 +60,21 @@ function date(v) {
|
|
|
60
60
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(text(v));
|
|
61
61
|
return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : null;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Date/datetime field → a LOCAL Date that KEEPS the stored wall-clock time, so
|
|
65
|
+
* `getHours()` / `toLocaleTimeString()` render the time as written. The stored
|
|
66
|
+
* value is a timezone-less workspace wall-clock (see the serialization note
|
|
67
|
+
* above), so it is read verbatim — no UTC conversion. Use this when the time
|
|
68
|
+
* matters (a check-in time, an appointment); `date` keeps only the calendar day.
|
|
69
|
+
* Parses `YYYY-MM-DD` with an optional `T`-or-space `HH:mm[:ss]`; a missing time
|
|
70
|
+
* is midnight. Null if absent or unparseable.
|
|
71
|
+
*/
|
|
72
|
+
function datetime(v) {
|
|
73
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}))?/.exec(text(v));
|
|
74
|
+
if (!m)
|
|
75
|
+
return null;
|
|
76
|
+
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), m[4] ? Number(m[4]) : 0, m[5] ? Number(m[5]) : 0);
|
|
77
|
+
}
|
|
63
78
|
/** One `{ id, display }` object → ResolvedLink, or null if absent/malformed. */
|
|
64
79
|
function asLink(v) {
|
|
65
80
|
if (!v || typeof v !== "object")
|
|
@@ -131,4 +146,4 @@ export function readLocked(rowValue) {
|
|
|
131
146
|
return false;
|
|
132
147
|
return bool(rowValue["__source_locked"]);
|
|
133
148
|
}
|
|
134
|
-
export const row = { opt, text, num, bool, date, link };
|
|
149
|
+
export const row = { opt, text, num, bool, date, datetime, link };
|
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 });
|
package/dist/src/select.d.ts
CHANGED
|
@@ -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
|