@colixsystems/widget-sdk 0.256.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,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
85
85
 
86
86
  ## Status
87
87
 
88
- `v0.256.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.
89
93
 
90
94
  ### What's new in 0.256.0 (contract 1.207.0)
91
95
 
@@ -215,7 +219,7 @@ const { groups, loading } = useDatastoreAggregate(tableId, { groupBy: "city", su
215
219
  data={groups.map((g) => ({ label: String(g.group), value: Number(g.sum) || 0 }))} />
216
220
  ```
217
221
 
218
- **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.
219
223
 
220
224
  ```jsx
221
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
  },
@@ -6374,7 +6374,10 @@ const CONTRACT = deepFreeze({
6374
6374
  // without it read light / false.
6375
6375
  // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6376
6376
  // "left" | "center" | "right" again; "center" centres the tabs.
6377
- version: "1.207.0",
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",
6378
6381
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6379
6382
  hooks: HOOKS,
6380
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
  },
@@ -6370,7 +6370,10 @@ const CONTRACT = deepFreeze({
6370
6370
  // without it read light / false.
6371
6371
  // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6372
6372
  // "left" | "center" | "right" again; "center" centres the tabs.
6373
- version: "1.207.0",
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",
6374
6377
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6375
6378
  hooks: HOOKS,
6376
6379
  primitives: PRIMITIVES,
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
  }
@@ -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.256.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
  },
@@ -6370,7 +6370,10 @@ const CONTRACT = deepFreeze({
6370
6370
  // without it read light / false.
6371
6371
  // 1.207.0: additive (sc-12198) — `resolveTopBarTokens().tabAlignment` is
6372
6372
  // "left" | "center" | "right" again; "center" centres the tabs.
6373
- version: "1.207.0",
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",
6374
6377
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6375
6378
  hooks: HOOKS,
6376
6379
  primitives: PRIMITIVES,