@colixsystems/widget-sdk 0.49.0 → 0.51.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 +11 -3
- package/dist/datetimepicker-format.js +53 -0
- package/dist/datetimepicker.js +28 -1
- package/dist/datetimepicker.native.js +85 -68
- package/dist/hooks.js +5 -2
- package/dist/index.d.ts +9 -0
- package/dist/lucideIconNames.cjs +173 -0
- package/dist/lucideIconNames.js +173 -0
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @colixsystems/widget-sdk
|
|
2
2
|
|
|
3
|
-
Common widget interface for
|
|
3
|
+
Common widget interface for AppStudio. This package is **core only** — it implements the contract that every widget (built-in or third-party, web or native) speaks: a `WidgetManifest`, a `WidgetContext`, a property schema, the primitives + rendering surface, the helper hooks, events, theme/i18n, and the static linter that gates submissions. **It owns no HTTP and depends on none of the data SDK packages.**
|
|
4
4
|
|
|
5
5
|
The data layer lives in **four separate domain-client packages**, each instantiated by the host and **injected into `WidgetContext`**. Widgets never import those packages — they reach the data surface only through this SDK's hooks, which read the injected client instances:
|
|
6
6
|
|
|
@@ -51,7 +51,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
51
51
|
|
|
52
52
|
## Status
|
|
53
53
|
|
|
54
|
-
`v0.
|
|
54
|
+
`v0.51.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
55
|
+
|
|
56
|
+
### What's new in 0.51.0
|
|
57
|
+
|
|
58
|
+
**`datastoreTemplate` tables can ship sample `rows` (sc-2070).** Each `WidgetDatastoreTemplateTable` now takes an optional `rows` array — sample data seeded into the table at install time so the widget renders with real content instead of an empty state. Each entry is an object keyed by column `name`; only the scalar/array column types are seedable (`STRING`, `TEXT`, `NUMBER`, `FLOAT`, `BOOL`, `DATE`, `STRING_ARRAY`, `INT_ARRAY`). RELATION, FILE, USER, and USER_GROUP columns are rejected at validation time (a sample row has no way to express their ids). A `null` value skips that cell; at most 25 rows per table. The "Seed data" action and every install path seed these rows in the same transaction that creates the tables. Additive — `rows` is optional and existing templates that omit it behave exactly as before.
|
|
55
59
|
|
|
56
60
|
### What's new in 0.49.0
|
|
57
61
|
|
|
@@ -102,7 +106,7 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
102
106
|
|
|
103
107
|
### What's new in 0.42.0
|
|
104
108
|
|
|
105
|
-
**RELATION columns hydrate with a display label (sc-1181).** Record reads now return `{ id, label }` for ONE_TO_ONE / ONE_TO_MANY and `[{ id, label }, ...]` for MANY_TO_MANY (empty array when no links) — `label` is the value of the column pointed at by the new optional `display_column_id` on `DatastoreSchemaColumn`, or, when unset, the first STRING/TEXT column on the target table. Widgets should render `record.<rel>.label` (or `record.<rel>.map(r => r.label).join(", ")` for M:M) directly; `.id` is still there for the foreign-key case. The cell-formatting helpers in the built-in `DataList
|
|
109
|
+
**RELATION columns hydrate with a display label (sc-1181).** Record reads now return `{ id, label }` for ONE_TO_ONE / ONE_TO_MANY and `[{ id, label }, ...]` for MANY_TO_MANY (empty array when no links) — `label` is the value of the column pointed at by the new optional `display_column_id` on `DatastoreSchemaColumn`, or, when unset, the first STRING/TEXT column on the target table. Widgets should render `record.<rel>.label` (or `record.<rel>.map(r => r.label).join(", ")` for M:M) directly; `.id` is still there for the foreign-key case. The cell-formatting helpers in the built-in `DataList` and `FieldValue` widgets already walk arrays and prefer `label` over `name` / `id` — author widgets that need the same can copy that pattern. `CONTRACT.version` is unchanged.
|
|
106
110
|
|
|
107
111
|
### What's new in 0.41.0
|
|
108
112
|
|
|
@@ -112,6 +116,10 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
112
116
|
|
|
113
117
|
**`useI18n().t()` no longer leaks the host's `{{t:key}}` miss placeholder.** Resolution steps 1–2 (per-widget and shared namespaces) already treated a `{{t:…}}` return from the host as a miss, but the final raw-key step did not — on the web Player (whose host resolver returns the placeholder form on a miss and ignores the fallback argument) a key absent from the tenant dictionary rendered as literal `{{t:key}}` text instead of degrading to `fallback ?? key`. The same guard now applies to all three steps. **The public contract is unchanged** — this is the behaviour `useI18n` always documented. `CONTRACT.version` is unchanged.
|
|
114
118
|
|
|
119
|
+
### What's new in 0.50.0
|
|
120
|
+
|
|
121
|
+
**`<DateTimePicker>` — tap the formatted date to open the picker (sc-1878).** The displayed date is now the affordance to change it on both hosts: on web, clicking the `<input>` text (or focusing it and pressing Enter / Space) calls `showPicker()` so the calendar opens from the text, not only the small built-in calendar icon; on native, the primitive renders an accessible, tappable formatted-date trigger that opens `@react-native-community/datetimepicker` on press (the RN library is imperative on Android, so the primitive owns the open/closed state — this also fixes the dialog auto-opening on mount). Both builds gained an `accessibilityLabel` prop that names the field for screen readers / test tooling. **The value contract is unchanged** — same `mode` values and ISO 8601 wire format on `value` / `onChange`; `CONTRACT.version` is unchanged.
|
|
122
|
+
|
|
115
123
|
### What's new in 0.40.1
|
|
116
124
|
|
|
117
125
|
**`<DateTimePicker>` actually renders on web (sc-1118).** The primitive was a single source that wrapped `@react-native-community/datetimepicker`, but that library ships iOS / Android only and has no react-native-web mapping — on the web Player and Studio the primitive rendered nothing, so date columns in the built-in **Form Input** / **Form Builder** widgets showed a label and required-asterisk with no input beneath them. The implementation is now split: native (`./datetimepicker.native.js`) still wraps the RN library; web (`./datetimepicker.js`) renders the browser's native `<input type="date|time|datetime-local">` directly. **The public contract is unchanged** — same component name, same `{ value, onChange, mode, minimumDate, maximumDate, disabled }` props, same ISO 8601 wire format on the value and the `onChange` callback. `CONTRACT.version` is unchanged.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — pure date/ISO helpers for the native `<DateTimePicker>`.
|
|
2
|
+
//
|
|
3
|
+
// Split out of datetimepicker.native.js so the formatting contract can be
|
|
4
|
+
// unit-tested without importing react-native / the RN datetimepicker library
|
|
5
|
+
// (neither is installed in this package's node tree). No React, no RN here —
|
|
6
|
+
// just Date math and string shaping.
|
|
7
|
+
|
|
8
|
+
export function parseToDate(value) {
|
|
9
|
+
if (value == null || value === "") return new Date();
|
|
10
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? new Date() : value;
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
const d = new Date(value);
|
|
13
|
+
return Number.isNaN(d.getTime()) ? new Date() : d;
|
|
14
|
+
}
|
|
15
|
+
return new Date();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatToIso(date, mode) {
|
|
19
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
|
|
20
|
+
if (mode === "date") {
|
|
21
|
+
// Local-date ISO (yyyy-mm-dd) — calendar dates are timezone-free so a
|
|
22
|
+
// "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
|
|
23
|
+
const y = date.getFullYear();
|
|
24
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
26
|
+
return `${y}-${m}-${d}`;
|
|
27
|
+
}
|
|
28
|
+
if (mode === "time") {
|
|
29
|
+
// Local-time ISO (hh:mm) — time-of-day is timezone-free for the same reason.
|
|
30
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
31
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
32
|
+
return `${h}:${mm}`;
|
|
33
|
+
}
|
|
34
|
+
// datetime — full UTC ISO so the wire format round-trips through a DATE column.
|
|
35
|
+
return date.toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The label shown on the tappable trigger. Empty value → a mode-specific
|
|
39
|
+
// placeholder so the field reads as "tap to pick" rather than blank.
|
|
40
|
+
export function formatDisplayLabel(value, mode) {
|
|
41
|
+
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
42
|
+
if (value == null || value === "") {
|
|
43
|
+
if (effectiveMode === "time") return "Select time";
|
|
44
|
+
if (effectiveMode === "datetime") return "Select date & time";
|
|
45
|
+
return "Select date";
|
|
46
|
+
}
|
|
47
|
+
if (effectiveMode === "date") return String(value).slice(0, 10);
|
|
48
|
+
if (effectiveMode === "time") return String(value).slice(0, 5);
|
|
49
|
+
// datetime — render the stored UTC ISO in the user's local clock.
|
|
50
|
+
const d = new Date(value);
|
|
51
|
+
if (Number.isNaN(d.getTime())) return String(value);
|
|
52
|
+
return `${formatToIso(d, "date")} ${formatToIso(d, "time")}`;
|
|
53
|
+
}
|
package/dist/datetimepicker.js
CHANGED
|
@@ -102,9 +102,33 @@ export function DateTimePicker({
|
|
|
102
102
|
? _isoToInputValue(maximumDate, effectiveMode)
|
|
103
103
|
: undefined;
|
|
104
104
|
|
|
105
|
+
// sc-1878 — clicking the formatted date text (or focusing it and pressing
|
|
106
|
+
// Enter / Space) opens the native calendar, not just the small built-in
|
|
107
|
+
// calendar icon. `showPicker()` requires a user gesture; click/keydown both
|
|
108
|
+
// qualify. Guarded: not every browser/build exposes it, and calling it while
|
|
109
|
+
// the picker is already open throws — neither should break the field.
|
|
110
|
+
const openPicker = (target) => {
|
|
111
|
+
if (disabled || !target || typeof target.showPicker !== "function") return;
|
|
112
|
+
try {
|
|
113
|
+
target.showPicker();
|
|
114
|
+
} catch {
|
|
115
|
+
// Already open, or blocked outside a user gesture — leave the input as-is.
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleClick = (event) => openPicker(event.currentTarget);
|
|
120
|
+
|
|
121
|
+
const handleKeyDown = (event) => {
|
|
122
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
openPicker(event.currentTarget);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
105
128
|
// Inline styles match the SDK's other web-only primitives — the host's
|
|
106
129
|
// form widgets wrap this in their own labelled field, so the input just
|
|
107
|
-
// needs to look like a normal text input.
|
|
130
|
+
// needs to look like a normal text input. `cursor: pointer` signals the
|
|
131
|
+
// whole field is the click affordance (sc-1878).
|
|
108
132
|
const style = {
|
|
109
133
|
boxSizing: "border-box",
|
|
110
134
|
width: "100%",
|
|
@@ -119,12 +143,15 @@ export function DateTimePicker({
|
|
|
119
143
|
borderColor: "rgba(0, 0, 0, 0.16)",
|
|
120
144
|
borderRadius: 6,
|
|
121
145
|
outline: "none",
|
|
146
|
+
cursor: disabled ? "default" : "pointer",
|
|
122
147
|
};
|
|
123
148
|
|
|
124
149
|
return React.createElement("input", {
|
|
125
150
|
type: inputType,
|
|
126
151
|
value: inputValue,
|
|
127
152
|
onChange: handleChange,
|
|
153
|
+
onClick: handleClick,
|
|
154
|
+
onKeyDown: handleKeyDown,
|
|
128
155
|
min,
|
|
129
156
|
max,
|
|
130
157
|
disabled: !!disabled,
|
|
@@ -1,65 +1,52 @@
|
|
|
1
|
-
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive.
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (native).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// Wraps `@react-native-community/datetimepicker`. That library is imperative
|
|
4
|
+
// on Android — rendering it shows the dialog immediately and re-mounting is
|
|
5
|
+
// the only way to reopen it — so the primitive owns a small open/closed state
|
|
6
|
+
// and renders a tappable, formatted-date trigger (sc-1878). Tapping the
|
|
7
|
+
// formatted date (the trigger is an accessible button, reachable by keyboard /
|
|
8
|
+
// screen-reader) opens the picker; choosing a value closes it and emits the
|
|
9
|
+
// ISO string. This mirrors the web build, where clicking the `<input>` text
|
|
10
|
+
// calls `showPicker()` — both hosts: tap the date, the picker opens.
|
|
9
11
|
//
|
|
10
|
-
// Props:
|
|
12
|
+
// Props (mirrors datetimepicker.js):
|
|
11
13
|
// value: string | null — ISO 8601 (`2026-05-28` for date mode,
|
|
12
|
-
// `
|
|
13
|
-
// `
|
|
14
|
+
// `14:30` for time mode,
|
|
15
|
+
// `2026-05-28T14:30:00.000Z` for datetime mode).
|
|
16
|
+
// `null` / "" shows the placeholder, opens at "now".
|
|
14
17
|
// onChange: (iso: string) => void
|
|
15
18
|
// mode: "date" | "time" | "datetime" — default "date"
|
|
16
19
|
// minimumDate / maximumDate: string | null — ISO bounds
|
|
17
20
|
// disabled: boolean
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// …and `day` ends up as an ISO string suitable for storing directly into
|
|
24
|
-
// a DATE column. The previous pattern of importing the RN library
|
|
25
|
-
// directly and managing `Date` objects in widget state is gone — the
|
|
26
|
-
// primitive normalizes both ends.
|
|
21
|
+
// accessibilityLabel: string | undefined — names the trigger button so the
|
|
22
|
+
// shared form-field renderer's column label reaches
|
|
23
|
+
// screen readers / test tooling, matching the web
|
|
24
|
+
// input's `aria-label`.
|
|
27
25
|
|
|
28
|
-
import React, { useMemo } from "react";
|
|
26
|
+
import React, { useMemo, useState } from "react";
|
|
29
27
|
// eslint-disable-next-line no-restricted-syntax
|
|
30
28
|
import RNDateTimePicker from "@react-native-community/datetimepicker";
|
|
29
|
+
import { Pressable, Text, StyleSheet } from "react-native";
|
|
30
|
+
import {
|
|
31
|
+
parseToDate,
|
|
32
|
+
formatToIso,
|
|
33
|
+
formatDisplayLabel,
|
|
34
|
+
} from "./datetimepicker-format.js";
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// so a "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
|
|
47
|
-
const y = date.getFullYear();
|
|
48
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
49
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
50
|
-
return `${y}-${m}-${d}`;
|
|
51
|
-
}
|
|
52
|
-
if (mode === "time") {
|
|
53
|
-
// Local-time ISO (hh:mm) — time-of-day is timezone-free for the same
|
|
54
|
-
// reason. Authors who want a full datetime get mode="datetime".
|
|
55
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
56
|
-
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
57
|
-
return `${h}:${mm}`;
|
|
58
|
-
}
|
|
59
|
-
// datetime — full UTC ISO so the wire format round-trips through the
|
|
60
|
-
// datastore's DATE column unchanged.
|
|
61
|
-
return date.toISOString();
|
|
62
|
-
}
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
trigger: {
|
|
38
|
+
minHeight: 44,
|
|
39
|
+
paddingVertical: 8,
|
|
40
|
+
paddingHorizontal: 12,
|
|
41
|
+
borderWidth: 1,
|
|
42
|
+
borderColor: "rgba(0, 0, 0, 0.16)",
|
|
43
|
+
borderRadius: 6,
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
},
|
|
46
|
+
triggerDisabled: { opacity: 0.5 },
|
|
47
|
+
label: { fontSize: 16 },
|
|
48
|
+
placeholder: { fontSize: 16, opacity: 0.5 },
|
|
49
|
+
});
|
|
63
50
|
|
|
64
51
|
export function DateTimePicker({
|
|
65
52
|
value,
|
|
@@ -68,35 +55,65 @@ export function DateTimePicker({
|
|
|
68
55
|
minimumDate,
|
|
69
56
|
maximumDate,
|
|
70
57
|
disabled,
|
|
58
|
+
accessibilityLabel,
|
|
71
59
|
}) {
|
|
72
60
|
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
73
|
-
const
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
const dateValue = useMemo(() => parseToDate(value), [value]);
|
|
74
63
|
const min = useMemo(
|
|
75
|
-
() => (minimumDate ?
|
|
64
|
+
() => (minimumDate ? parseToDate(minimumDate) : undefined),
|
|
76
65
|
[minimumDate],
|
|
77
66
|
);
|
|
78
67
|
const max = useMemo(
|
|
79
|
-
() => (maximumDate ?
|
|
68
|
+
() => (maximumDate ? parseToDate(maximumDate) : undefined),
|
|
80
69
|
[maximumDate],
|
|
81
70
|
);
|
|
82
71
|
|
|
83
|
-
const
|
|
72
|
+
const isEmpty = value == null || value === "";
|
|
73
|
+
|
|
74
|
+
const handleChange = (event, picked) => {
|
|
75
|
+
// Android fires "dismissed" when the user cancels; close without emitting.
|
|
76
|
+
setOpen(false);
|
|
77
|
+
if (event?.type === "dismissed") return;
|
|
84
78
|
if (typeof onChange !== "function") return;
|
|
85
79
|
if (!(picked instanceof Date)) return;
|
|
86
|
-
const iso =
|
|
80
|
+
const iso = formatToIso(picked, effectiveMode);
|
|
87
81
|
if (iso != null) onChange(iso);
|
|
88
82
|
};
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
const trigger = React.createElement(
|
|
85
|
+
Pressable,
|
|
86
|
+
{
|
|
87
|
+
onPress: () => { if (!disabled) setOpen(true); },
|
|
88
|
+
disabled: !!disabled,
|
|
89
|
+
accessibilityRole: "button",
|
|
90
|
+
accessibilityLabel,
|
|
91
|
+
accessibilityState: { disabled: !!disabled },
|
|
92
|
+
style: [styles.trigger, disabled && styles.triggerDisabled],
|
|
93
|
+
},
|
|
94
|
+
React.createElement(
|
|
95
|
+
Text,
|
|
96
|
+
{ style: isEmpty ? styles.placeholder : styles.label },
|
|
97
|
+
formatDisplayLabel(value, effectiveMode),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!open) return trigger;
|
|
102
|
+
|
|
103
|
+
// The RN library's `mode` is "date" / "time"; iOS also honours "datetime",
|
|
104
|
+
// Android falls back to date-only for it (a pre-existing limit, unchanged
|
|
105
|
+
// here). The web build uses datetimepicker.js, so this file is never web.
|
|
106
|
+
return React.createElement(
|
|
107
|
+
React.Fragment,
|
|
108
|
+
null,
|
|
109
|
+
trigger,
|
|
110
|
+
React.createElement(RNDateTimePicker, {
|
|
111
|
+
value: dateValue,
|
|
112
|
+
mode: effectiveMode,
|
|
113
|
+
minimumDate: min,
|
|
114
|
+
maximumDate: max,
|
|
115
|
+
disabled: !!disabled,
|
|
116
|
+
onChange: handleChange,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
102
119
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -569,8 +569,11 @@ function toDatastoreError(err) {
|
|
|
569
569
|
* resolves to the `{ data, meta }` list envelope VERBATIM (the client no
|
|
570
570
|
* longer unwraps to a bare array), so we read `res.data` (defaulting to `[]`)
|
|
571
571
|
* into component state. Author column values inside each row keep whatever
|
|
572
|
-
* the author named them.
|
|
573
|
-
*
|
|
572
|
+
* the author named them. `query.sort` accepts `"col:desc"` or `{ field, dir }`;
|
|
573
|
+
* `query.filter` accepts a `{ col: "op:value" }` map or a `[{ column,
|
|
574
|
+
* operator, value }]` array — the client normalises both to the wire form. We
|
|
575
|
+
* re-fetch when [table, JSON.stringify(query)] changes. `refetch` re-runs the
|
|
576
|
+
* same call on demand.
|
|
574
577
|
*
|
|
575
578
|
* When `table` is falsy (e.g. the user hasn't bound a `tableRef` property
|
|
576
579
|
* yet), the hook resolves to { data: [], loading: false, error: null,
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,15 @@ export interface WidgetDatastoreTemplateTable {
|
|
|
168
168
|
*/
|
|
169
169
|
publicGrant?: { canRead?: boolean; canWrite?: boolean; canDelete?: boolean };
|
|
170
170
|
columns: WidgetDatastoreTemplateColumn[];
|
|
171
|
+
/**
|
|
172
|
+
* Optional sample rows seeded into the table at install time so the widget
|
|
173
|
+
* renders with real data instead of an empty state. Each key is a column
|
|
174
|
+
* `name`; only non-relation/file/user columns are seedable (RELATION, FILE,
|
|
175
|
+
* USER, USER_GROUP are rejected). `null` skips that cell. Max 25 rows.
|
|
176
|
+
*/
|
|
177
|
+
rows?: Array<
|
|
178
|
+
Record<string, string | number | boolean | string[] | number[] | null>
|
|
179
|
+
>;
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
export interface WidgetDatastoreTemplate {
|