@colixsystems/widget-sdk 0.256.0 → 0.259.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/detail-parts-view.js +13 -10
- package/dist/index.d.ts +8 -3
- package/dist/markdown.js +59 -2
- 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.259.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.259.0 (contract 1.210.0)
|
|
91
|
+
|
|
92
|
+
**`<Avatar url>` falls back to the initials** when its picture fails to load, instead of showing a broken image (as `<Avatar fileId>` already did). The built-in form fields for users, groups and related records now use `<PeoplePicker>`, and the User widget's photo uses `<Avatar>`.
|
|
93
|
+
|
|
94
|
+
### What's new in 0.258.0 (contract 1.209.0)
|
|
95
|
+
|
|
96
|
+
**`<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
97
|
|
|
90
98
|
### What's new in 0.256.0 (contract 1.207.0)
|
|
91
99
|
|
|
@@ -215,7 +223,7 @@ const { groups, loading } = useDatastoreAggregate(tableId, { groupBy: "city", su
|
|
|
215
223
|
data={groups.map((g) => ({ label: String(g.group), value: Number(g.sum) || 0 }))} />
|
|
216
224
|
```
|
|
217
225
|
|
|
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 `
|
|
226
|
+
**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
227
|
|
|
220
228
|
```jsx
|
|
221
229
|
<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,12 @@ 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
|
-
|
|
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
|
+
// 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
|
|
6381
|
+
// initials when its picture fails to load. No API change.
|
|
6382
|
+
version: "1.210.0",
|
|
6378
6383
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6379
6384
|
hooks: HOOKS,
|
|
6380
6385
|
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,12 @@ 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
|
-
|
|
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
|
+
// 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
|
|
6377
|
+
// initials when its picture fails to load. No API change.
|
|
6378
|
+
version: "1.210.0",
|
|
6374
6379
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6375
6380
|
hooks: HOOKS,
|
|
6376
6381
|
primitives: PRIMITIVES,
|
|
@@ -111,6 +111,13 @@ export function makeDetailParts(rn) {
|
|
|
111
111
|
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// A photo that cannot load gives way to the initials.
|
|
115
|
+
function UrlAvatar({ photo, monogram }) {
|
|
116
|
+
const [failed, setFailed] = React.useState(false);
|
|
117
|
+
if (failed) return monogram;
|
|
118
|
+
return h(Image, { ...photo, onError: () => setFailed(true) });
|
|
119
|
+
}
|
|
120
|
+
|
|
114
121
|
function Avatar({ name, url, fileId, seed, size = "md", shape = "circle", accessibilityLabel, style, testID }) {
|
|
115
122
|
const { colors, radii, typography } = useDetailTheme();
|
|
116
123
|
const px = avatarPixelSize(size);
|
|
@@ -127,15 +134,6 @@ export function makeDetailParts(rn) {
|
|
|
127
134
|
const stored = typeof fileId === "string" && fileId !== "";
|
|
128
135
|
const source = typeof url === "string" ? url.trim() : "";
|
|
129
136
|
const label = accessibilityLabel || captionText(name) || "Avatar";
|
|
130
|
-
if (source && !stored) {
|
|
131
|
-
return h(Image, {
|
|
132
|
-
source: { uri: source },
|
|
133
|
-
accessibilityLabel: label,
|
|
134
|
-
resizeMode: "cover",
|
|
135
|
-
style: [box, style],
|
|
136
|
-
testID,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
137
|
const initials = initialsFor(name);
|
|
140
138
|
const fallback = { alignItems: "center", justifyContent: "center" };
|
|
141
139
|
const content = initials
|
|
@@ -152,11 +150,16 @@ export function makeDetailParts(rn) {
|
|
|
152
150
|
initials,
|
|
153
151
|
)
|
|
154
152
|
: h(Icon, { name: "user", size: Math.round(px * 0.55), color: ink });
|
|
153
|
+
const monogram = h(View, { style: [box, fallback, style], accessibilityLabel: label, testID }, content);
|
|
154
|
+
if (source && !stored) {
|
|
155
|
+
const photo = { source: { uri: source }, accessibilityLabel: label, resizeMode: "cover", style: [box, style], testID };
|
|
156
|
+
return h(UrlAvatar, { key: source, photo, monogram });
|
|
157
|
+
}
|
|
155
158
|
if (stored) {
|
|
156
159
|
const frame = { style: [box, fallback, { overflow: "hidden" }, style], accessibilityLabel: label, testID };
|
|
157
160
|
return h(FileAvatar, { key: fileId, fileId, px, frame, monogram: content });
|
|
158
161
|
}
|
|
159
|
-
return
|
|
162
|
+
return monogram;
|
|
160
163
|
}
|
|
161
164
|
Avatar.displayName = "Avatar";
|
|
162
165
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4151,7 +4151,7 @@ export const Avatar: (props: {
|
|
|
4151
4151
|
name?: string;
|
|
4152
4152
|
/** A stable id (a user id): tints the monogram with that person's colour. */
|
|
4153
4153
|
seed?: string;
|
|
4154
|
-
/** An absolute image URL. */
|
|
4154
|
+
/** An absolute image URL; the initials show if it cannot load. */
|
|
4155
4155
|
url?: string;
|
|
4156
4156
|
/**
|
|
4157
4157
|
* A filestore file id (a FILE column's value). Takes precedence over `url`;
|
|
@@ -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
|
}
|
|
@@ -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.259.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,12 @@ 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
|
-
|
|
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
|
+
// 1.210.0: behaviour (sc-12245) — `<Avatar url>` falls back to the
|
|
6377
|
+
// initials when its picture fails to load. No API change.
|
|
6378
|
+
version: "1.210.0",
|
|
6374
6379
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6375
6380
|
hooks: HOOKS,
|
|
6376
6381
|
primitives: PRIMITIVES,
|