@colixsystems/widget-sdk 0.255.0 → 0.258.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 CHANGED
@@ -85,7 +85,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
85
85
 
86
86
  ## Status
87
87
 
88
- `v0.255.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**.
88
+ `v0.258.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**.
89
+
90
+ ### What's new in 0.258.0 (contract 1.209.0)
91
+
92
+ **`<PeoplePicker>` options for admin flows.** `activeOnly` keeps deactivated people out of a users picker; `loadErrorText` may be a function of the error, so a picker can say why a read was refused; each option carries `<testID>-option-<id>`. The built-in User Management now uses `<PeoplePicker>` for adding members and choosing invite groups, a `<Menu>` for row actions, and `<ConfirmDialog>` before removing a user or deleting a group.
93
+
94
+ ### What's new in 0.256.0 (contract 1.207.0)
95
+
96
+ **A page's top navigation bar can centre its tabs again.** `resolveTopBarTokens().tabAlignment` is `"left"` | `"center"` | `"right"`. A `theme_config.topBar.tabAlignment` of `"center"` places the tabs between two flexible spacers, never by centring the row, so a row with more tabs than fit still scrolls from its first tab. Unset or unknown still resolves to `"left"`, so an app that never chose is unchanged. Host-integration surface only — no author-facing hook, prop, primitive, or manifest field changed.
89
97
 
90
98
  ### What's new in 0.255.0
91
99
 
@@ -211,7 +219,7 @@ const { groups, loading } = useDatastoreAggregate(tableId, { groupBy: "city", su
211
219
  data={groups.map((g) => ({ label: String(g.group), value: Number(g.sum) || 0 }))} />
212
220
  ```
213
221
 
214
- **Searchable pickers: `<Select searchable>` and `<PeoplePicker>`.** A long `Select` can be searched: pass `searchable` to filter the options you hold, `onSearch` to fetch each query yourself (called with "" when the sheet opens and again once typing pauses), or an async `options` function and Select fetches, keeping only the latest answer. `<PeoplePicker source="users" | "groups" | "records">` does this for the workspace's people, groups or a table's records with avatar rows, server-side search and `excludeIds`, and names a saved choice that is not on the first page instead of showing its id (a malformed saved id reads "Unknown" without failing the rest). `useGroups({ ids })` names groups by id.
222
+ **Searchable pickers: `<Select searchable>` and `<PeoplePicker>`.** A long `Select` can be searched: pass `searchable` to filter the options you hold, `onSearch` to fetch each query yourself (called with "" when the sheet opens and again once typing pauses), or an async `options` function and Select fetches, keeping only the latest answer. `<PeoplePicker source="users" | "groups" | "records">` does this for the workspace's people, groups or a table's records with avatar rows, server-side search, `excludeIds` and (for users) `activeOnly`, and names a saved choice that is not on the first page instead of showing its id (a malformed saved id reads "Unknown" without failing the rest). `useGroups({ ids })` names groups by id.
215
223
 
216
224
  ```jsx
217
225
  <PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />
package/dist/contract.cjs CHANGED
@@ -3577,7 +3577,7 @@ const PRIMITIVES = [
3577
3577
  {
3578
3578
  name: "PeoplePicker",
3579
3579
  description:
3580
- 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3580
+ 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `activeOnly` leaves deactivated users out, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. `loadErrorText` may be a function of the failed search\'s error (`error.code === "FORBIDDEN"` for a refused read). Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3581
3581
  rnComponent: null,
3582
3582
  docsUrl: null,
3583
3583
  },
@@ -6372,7 +6372,12 @@ const CONTRACT = deepFreeze({
6372
6372
  // showing, the app's appearance preference, `canSwitch` and
6373
6373
  // `setPreference`, backed by the optional `appearance` host slice. Hosts
6374
6374
  // without it read light / false.
6375
- version: "1.206.0",
6375
+ // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6376
+ // "left" | "center" | "right" again; "center" centres the tabs.
6377
+ // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6378
+ // (users), a function `loadErrorText(error)`, and gives each option a
6379
+ // `<testID>-option-<id>`.
6380
+ version: "1.209.0",
6376
6381
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6377
6382
  hooks: HOOKS,
6378
6383
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -3577,7 +3577,7 @@ const PRIMITIVES = [
3577
3577
  {
3578
3578
  name: "PeoplePicker",
3579
3579
  description:
3580
- 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3580
+ 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `activeOnly` leaves deactivated users out, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. `loadErrorText` may be a function of the failed search\'s error (`error.code === "FORBIDDEN"` for a refused read). Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3581
3581
  rnComponent: null,
3582
3582
  docsUrl: null,
3583
3583
  },
@@ -6368,7 +6368,12 @@ const CONTRACT = deepFreeze({
6368
6368
  // showing, the app's appearance preference, `canSwitch` and
6369
6369
  // `setPreference`, backed by the optional `appearance` host slice. Hosts
6370
6370
  // without it read light / false.
6371
- version: "1.206.0",
6371
+ // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6372
+ // "left" | "center" | "right" again; "center" centres the tabs.
6373
+ // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6374
+ // (users), a function `loadErrorText(error)`, and gives each option a
6375
+ // `<testID>-option-<id>`.
6376
+ version: "1.209.0",
6372
6377
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6373
6378
  hooks: HOOKS,
6374
6379
  primitives: PRIMITIVES,
package/dist/host.d.ts CHANGED
@@ -409,7 +409,7 @@ export interface TopBarTokens {
409
409
  * current tab so the page shows through it. */
410
410
  tabStyle: "underline" | "attached";
411
411
  /** Where the row's tabs sit across its width; `left` unless authored. */
412
- tabAlignment: "left" | "right";
412
+ tabAlignment: "left" | "center" | "right";
413
413
  /** The column the tabs sit in: the whole bar, or the page content's. The
414
414
  * bar's surface spans the full width either way. */
415
415
  tabRowWidth: "full" | "content";
package/dist/index.d.ts CHANGED
@@ -4613,7 +4613,8 @@ export type PeoplePickerSource = "users" | "groups" | "records";
4613
4613
  * Nothing loads until the sheet opens; each pause in typing searches on the
4614
4614
  * server. A saved value the page does not hold is looked up once by id, so the
4615
4615
  * trigger shows its name, `loadingText` meanwhile, or `unknownText` when it is
4616
- * gone, never the id. Every other `<Select>` prop works.
4616
+ * gone, never the id. Every other `<Select>` prop works; with a `testID`
4617
+ * each option takes `<testID>-option-<id>`.
4617
4618
  *
4618
4619
  * @example
4619
4620
  * <PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />
@@ -4622,7 +4623,7 @@ export type PeoplePickerSource = "users" | "groups" | "records";
4622
4623
  * onValueChange={setCustomerId} />
4623
4624
  */
4624
4625
  export const PeoplePicker: (
4625
- props: Omit<SelectBaseProps<string>, "options" | "onSearch" | "selectedOptions" | "searchable"> & {
4626
+ props: Omit<SelectBaseProps<string>, "options" | "onSearch" | "selectedOptions" | "searchable" | "loadErrorText"> & {
4626
4627
  /** Default "users". */
4627
4628
  source?: PeoplePickerSource;
4628
4629
  /** `source="records"`: the table id. */
@@ -4633,6 +4634,10 @@ export const PeoplePicker: (
4633
4634
  avatarColumn?: string;
4634
4635
  /** `source="users"`: only members of this group (a `useGroups()` row id). */
4635
4636
  groupId?: string;
4637
+ /** `source="users"`: leave deactivated users out. */
4638
+ activeOnly?: boolean;
4639
+ /** Shown when a search fails; a function names the failure, e.g. a refused read. */
4640
+ loadErrorText?: string | ((error: { code?: string; message?: string }) => string);
4636
4641
  /** `source="records"`: narrows the rows, as `useDatastoreQuery`'s `filter`. */
4637
4642
  filter?: Query["filter"];
4638
4643
  /** Ids never offered (already members, the signed-in user). */
package/dist/markdown.js CHANGED
@@ -29,6 +29,9 @@ const ORDERED_RE = /^\s*(\d+)[.)]\s+(.*)$/;
29
29
  const TABLE_ROW_RE = /^\s*\|(.*)\|\s*$/;
30
30
  const TABLE_DIVIDER_RE = /^\s*\|(?:\s*:?-+:?\s*\|)+\s*$/;
31
31
 
32
+ const FENCE_OPEN_RE = /^\s*```+\s*([^`]*)$/;
33
+ const FENCE_CLOSE_RE = /^\s*```+\s*$/;
34
+
32
35
  // An image is its own block, written on one line with the size in the title
33
36
  // slot. Keeping blocks in the SAME markdown column as the prose is what lets
34
37
  // content written before images existed keep rendering with no migration.
@@ -322,6 +325,60 @@ export function formatMarkdownTable(columns = 3, rows = 2) {
322
325
  return out.join("\n");
323
326
  }
324
327
 
328
+ // Any `[label](target)` as written, a label wrapped over lines included.
329
+ const RAW_LINK_RE = /(!?)\[((?:[^\]\\]|\\.)*)\]\(\s*(\S+?)\s*\)/g;
330
+
331
+ function collectHrefs(spans, into) {
332
+ for (const span of spans || []) if (span.href) into.add(span.href);
333
+ }
334
+
335
+ /**
336
+ * sc-12618 — `text` with each link to a target `keepLink` refuses turned back
337
+ * into its label, everything else byte for byte.
338
+ *
339
+ * `keepLink` is asked only about the targets `<RichText>` draws as links. A
340
+ * refused target is then unlinked wherever it is written outside a fenced
341
+ * block, so no reading of the text (a label wrapped over two lines, a link
342
+ * only a table cell or a joined paragraph exposes) leaves one live. An
343
+ * occurrence the renderer would not have drawn loses only its brackets.
344
+ */
345
+ export function unlinkMarkdownLinks(text, keepLink) {
346
+ if (typeof text !== "string" || !text.includes("](")) return text;
347
+ const hrefs = new Set();
348
+ for (const block of parseMarkdown(text, { stripLegacyHtml: false })) {
349
+ collectHrefs(block.spans, hrefs);
350
+ for (const row of block.rows || []) for (const cell of row.cells) collectHrefs(cell, hrefs);
351
+ }
352
+ const refused = new Set([...hrefs].filter((href) => !keepLink(href)));
353
+ if (refused.size === 0) return text;
354
+
355
+ const unlink = (chunk) =>
356
+ chunk.replace(RAW_LINK_RE, (whole, bang, label, target) =>
357
+ bang || !refused.has(target) ? whole : label,
358
+ );
359
+ const out = [];
360
+ let prose = [];
361
+ let inFence = false;
362
+ const flushProse = () => {
363
+ if (prose.length > 0) out.push(unlink(prose.join("\n")));
364
+ prose = [];
365
+ };
366
+ for (const line of text.split("\n")) {
367
+ if (inFence) {
368
+ out.push(line);
369
+ if (FENCE_CLOSE_RE.test(line)) inFence = false;
370
+ } else if (FENCE_OPEN_RE.test(line.trimEnd())) {
371
+ flushProse();
372
+ out.push(line);
373
+ inFence = true;
374
+ } else {
375
+ prose.push(line);
376
+ }
377
+ }
378
+ flushProse();
379
+ return out.join("\n");
380
+ }
381
+
325
382
  /**
326
383
  * Parses markdown text into renderable blocks:
327
384
  * `{ type: "heading"|"paragraph"|"bullet"|"ordered", level?, marker?, spans }`,
@@ -360,12 +417,12 @@ export function parseMarkdown(text, options) {
360
417
 
361
418
  // A fenced block is verbatim: its newlines and indentation ARE the content,
362
419
  // and joining them into a paragraph made a code sample unreadable.
363
- const fence = /^\s*```+\s*([^`]*)$/.exec(line);
420
+ const fence = FENCE_OPEN_RE.exec(line);
364
421
  if (fence) {
365
422
  flushParagraph();
366
423
  const body = [];
367
424
  let j = i + 1;
368
- while (j < lines.length && !/^\s*```+\s*$/.test(lines[j])) {
425
+ while (j < lines.length && !FENCE_CLOSE_RE.test(lines[j])) {
369
426
  body.push(lines[j]);
370
427
  j += 1;
371
428
  }
@@ -566,11 +566,8 @@ function resolveTopStripeLayout(theme) {
566
566
  // reading the bar; so does the SITE header, which is shown instead of this
567
567
  // row rather than beside it.
568
568
  //
569
- // The notification bell is the one member of `slots` this does not reach.
570
- // It keeps its own resting colour on both hosts — web reads the authored
571
- // `topBar.textColor` and the export tints it not at all — and moving it
572
- // here would widen that difference rather than close it. Whoever closes it
573
- // should move both hosts at once.
569
+ // The notification bell is one of the row's icons and takes this tint on
570
+ // both hosts; the bells outside the row keep the bar's.
574
571
  tintColor: hexOr(stripe.textColor, barTokens.tintColor),
575
572
  titleColor: hexOr(stripe.textColor, barTokens.titleColor),
576
573
  order,
@@ -617,7 +614,7 @@ function resolveAppLogo(logo, darkSurfaceLogo, onDark) {
617
614
  return onDark && darkSurfaceLogo ? darkSurfaceLogo : logo;
618
615
  }
619
616
 
620
- const TAB_ALIGNMENTS = ["left", "right"];
617
+ const TAB_ALIGNMENTS = ["left", "center", "right"];
621
618
  const TAB_ROW_WIDTHS = ["full", "content"];
622
619
 
623
620
  function resolveTopBarTokens(theme) {
@@ -557,11 +557,8 @@ export function resolveTopStripeLayout(theme) {
557
557
  // reading the bar; so does the SITE header, which is shown instead of this
558
558
  // row rather than beside it.
559
559
  //
560
- // The notification bell is the one member of `slots` this does not reach.
561
- // It keeps its own resting colour on both hosts — web reads the authored
562
- // `topBar.textColor` and the export tints it not at all — and moving it
563
- // here would widen that difference rather than close it. Whoever closes it
564
- // should move both hosts at once.
560
+ // The notification bell is one of the row's icons and takes this tint on
561
+ // both hosts; the bells outside the row keep the bar's.
565
562
  tintColor: hexOr(stripe.textColor, barTokens.tintColor),
566
563
  titleColor: hexOr(stripe.textColor, barTokens.titleColor),
567
564
  order,
@@ -608,7 +605,7 @@ export function resolveAppLogo(logo, darkSurfaceLogo, onDark) {
608
605
  return onDark && darkSurfaceLogo ? darkSurfaceLogo : logo;
609
606
  }
610
607
 
611
- const TAB_ALIGNMENTS = ["left", "right"];
608
+ const TAB_ALIGNMENTS = ["left", "center", "right"];
612
609
  const TAB_ROW_WIDTHS = ["full", "content"];
613
610
 
614
611
  export function resolveTopBarTokens(theme) {
@@ -27,14 +27,16 @@ function text(value) {
27
27
  * server, never filtered from one page here.
28
28
  *
29
29
  * @param {"users"|"groups"|"records"} source
30
- * @param {{query?: string, pageSize?: number, groupId?: string, filter?: object}} own
30
+ * @param {{query?: string, pageSize?: number, groupId?: string, filter?: object,
31
+ * activeOnly?: boolean}} own
31
32
  */
32
- export function pageQuery(source, { query, pageSize, groupId, filter } = {}) {
33
+ export function pageQuery(source, { query, pageSize, groupId, filter, activeOnly } = {}) {
33
34
  const q = text(query);
34
35
  const limit =
35
36
  Number.isInteger(pageSize) && pageSize > 0 ? Math.min(pageSize, 200) : DEFAULT_PEOPLE_PAGE_SIZE;
36
37
  const out = { ...(q ? { q } : null), limit };
37
38
  if (source === "users" && isId(groupId)) out.group_id = groupId;
39
+ if (source === "users" && activeOnly === true) out.is_active = true;
38
40
  if (source === "records" && filter && typeof filter === "object") out.filter = filter;
39
41
  return out;
40
42
  }
@@ -86,6 +88,19 @@ export function rowLabel(row, { source, labelColumn, unknownText }) {
86
88
  return text(row.name) || unknownText;
87
89
  }
88
90
 
91
+ /**
92
+ * What `<Select>` is told when a page fails: a `loadErrorText` function
93
+ * names that failure, a string replaces the default, anything else keeps it.
94
+ *
95
+ * @param {string|((error: object) => string)|undefined} loadErrorText
96
+ * @param {object|null} error
97
+ */
98
+ export function pageErrorText(loadErrorText, error) {
99
+ if (!error) return false;
100
+ if (typeof loadErrorText === "function") return text(loadErrorText(error)) || true;
101
+ return typeof loadErrorText === "string" && loadErrorText ? loadErrorText : true;
102
+ }
103
+
89
104
  /** The rows a page offers: each with an id, none of `excludeIds`. */
90
105
  export function offeredRows(rows, excludeIds) {
91
106
  const excluded = new Set(Array.isArray(excludeIds) ? excludeIds.map(String) : []);
@@ -20,6 +20,7 @@ import {
20
20
  answersQuery,
21
21
  lookupableIds,
22
22
  offeredRows,
23
+ pageErrorText,
23
24
  pageQuery,
24
25
  pickedIds,
25
26
  pickedLabels,
@@ -148,12 +149,14 @@ export function makePeoplePicker(rn) {
148
149
  labelColumn,
149
150
  avatarColumn,
150
151
  groupId,
152
+ activeOnly,
151
153
  filter,
152
154
  excludeIds,
153
155
  pageSize,
154
156
  showAvatars = true,
155
157
  unknownText = "Unknown",
156
158
  loadingText = "Loading…",
159
+ loadErrorText,
157
160
  multiple = false,
158
161
  value,
159
162
  values,
@@ -173,6 +176,7 @@ export function makePeoplePicker(rn) {
173
176
  return {
174
177
  value: row.id,
175
178
  label,
179
+ testID: rest.testID ? `${rest.testID}-option-${row.id}` : undefined,
176
180
  ...(showAvatars ? { leading: decoration(avatarFor(source, row, label, avatarColumn)) } : null),
177
181
  };
178
182
  };
@@ -193,7 +197,7 @@ export function makePeoplePicker(rn) {
193
197
  const missing = readable ? unresolvedIds(ids, knownRows, attempted) : [];
194
198
  const { lookup, unknown } = lookupableIds(missing, source);
195
199
  const named = unknown.length ? new Set([...attempted, ...unknown.map(String)]) : attempted;
196
- const query = sent === null || !readable ? null : pageQuery(source, { query: sent, pageSize, groupId, filter });
200
+ const query = sent === null || !readable ? null : pageQuery(source, { query: sent, pageSize, groupId, filter, activeOnly });
197
201
  const options = offeredRows(page.rows, excludeIds).map(toOption);
198
202
  const selectedOptions = pickedLabels(ids, { knownRows, attempted: named, nameOf, loadingText, unknownText }).map(
199
203
  (own) => (own.row ? toOption(own.row) : own),
@@ -221,7 +225,7 @@ export function makePeoplePicker(rn) {
221
225
  selectedOptions,
222
226
  onSearch: setSent,
223
227
  loading: sent !== null && (page.query !== sent || page.loading),
224
- optionsError: !!page.error,
228
+ optionsError: pageErrorText(loadErrorText, page.error),
225
229
  loadingText,
226
230
  }),
227
231
  query ? h(pages[source], { table, query, sent, onReport }) : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.255.0",
3
+ "version": "0.258.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -3577,7 +3577,7 @@ const PRIMITIVES = [
3577
3577
  {
3578
3578
  name: "PeoplePicker",
3579
3579
  description:
3580
- 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3580
+ 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `activeOnly` leaves deactivated users out, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. `loadErrorText` may be a function of the failed search\'s error (`error.code === "FORBIDDEN"` for a refused read). Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3581
3581
  rnComponent: null,
3582
3582
  docsUrl: null,
3583
3583
  },
@@ -6368,7 +6368,12 @@ const CONTRACT = deepFreeze({
6368
6368
  // showing, the app's appearance preference, `canSwitch` and
6369
6369
  // `setPreference`, backed by the optional `appearance` host slice. Hosts
6370
6370
  // without it read light / false.
6371
- version: "1.206.0",
6371
+ // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6372
+ // "left" | "center" | "right" again; "center" centres the tabs.
6373
+ // 1.209.0: additive (sc-12242) — `<PeoplePicker>` takes `activeOnly`
6374
+ // (users), a function `loadErrorText(error)`, and gives each option a
6375
+ // `<testID>-option-<id>`.
6376
+ version: "1.209.0",
6372
6377
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6373
6378
  hooks: HOOKS,
6374
6379
  primitives: PRIMITIVES,