@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 +10 -2
- package/dist/contract.cjs +7 -2
- package/dist/contract.js +7 -2
- package/dist/host.d.ts +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/markdown.js +59 -2
- package/dist/navigation.cjs +3 -6
- package/dist/navigation.js +3 -6
- package/dist/people-picker-logic.js +17 -2
- package/dist/people-picker-view.js +6 -2
- package/package.json +1 -1
- package/src/contract.js +7 -2
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.
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 &&
|
|
425
|
+
while (j < lines.length && !FENCE_CLOSE_RE.test(lines[j])) {
|
|
369
426
|
body.push(lines[j]);
|
|
370
427
|
j += 1;
|
|
371
428
|
}
|
package/dist/navigation.cjs
CHANGED
|
@@ -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
|
|
570
|
-
//
|
|
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) {
|
package/dist/navigation.js
CHANGED
|
@@ -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
|
|
561
|
-
//
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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,
|