@colixsystems/widget-sdk 0.212.0 → 0.214.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 +56 -2
- package/dist/button-view.js +5 -3
- package/dist/confirm-dialog-view.js +158 -0
- package/dist/confirm-dialog.js +11 -0
- package/dist/confirm-dialog.native.js +11 -0
- package/dist/contract.cjs +47 -4
- package/dist/contract.js +47 -4
- package/dist/index.d.ts +89 -1
- package/dist/index.js +3 -0
- package/dist/index.native.js +3 -0
- package/dist/stats-hooks.js +22 -3
- package/package.json +1 -1
- package/src/contract.js +47 -4
package/README.md
CHANGED
|
@@ -83,7 +83,60 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
83
83
|
|
|
84
84
|
## Status
|
|
85
85
|
|
|
86
|
-
`v0.
|
|
86
|
+
`v0.214.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**.
|
|
87
|
+
|
|
88
|
+
### What's new in 0.214.0 (contract 1.173.0)
|
|
89
|
+
|
|
90
|
+
**Who is behind a figure — `useActiveUsers({ within, group_id, limit, offset })`.** One page of the users seen within `within` — `"online"` (default; the app open in the last two minutes), `"day"` (since midnight in the workspace's time zone), `"week"` or `"month"` (the last 7 / 30 days) — most recently seen first, as `[{ id, name, last_active_at, last_platform }]` with `total` for paging. Same gate as `useUserActivity` (`users.activity:*` plus the "View user activity" permission); pass `null` to read nothing.
|
|
91
|
+
|
|
92
|
+
**"Who joined" — `useUsers({ joined_since, sort: "newest" })`.** The admin user list narrows to users created since `joined_since` (a `YYYY-MM-DD` date read in the workspace's time zone, or an ISO instant) and lists them newest first. Join dates are admin data, so a caller with only the directory view gets the plain list.
|
|
93
|
+
|
|
94
|
+
**Reminding several — `remind(id, message, { refetch: false })`.** `useInactiveUsers`' `remind` re-reads the page after each reminder; pass `{ refetch: false }` while sending a batch and call `refetch()` once at the end.
|
|
95
|
+
|
|
96
|
+
### What's new in 0.213.0 (contract 1.172.0)
|
|
97
|
+
|
|
98
|
+
**New component `<ConfirmDialog>` — the standard "are you sure?".** Widgets asked for confirmation each in their own way (a button that re-labels itself to be pressed twice, a hand-laid Yes / No row), so a delete looked and behaved differently from one widget to the next. `<ConfirmDialog>` is that prompt, built once as a preset of `<Modal>` and `<Button>`:
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
import { useState } from "react";
|
|
102
|
+
import { ConfirmDialog, Button, useToast } from "@colixsystems/widget-sdk";
|
|
103
|
+
|
|
104
|
+
function DeleteOrder({ order, onDelete }) {
|
|
105
|
+
const [asking, setAsking] = useState(false);
|
|
106
|
+
const { showToast } = useToast();
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<Button variant="danger" title="Delete" onPress={() => setAsking(true)} />
|
|
110
|
+
<ConfirmDialog
|
|
111
|
+
visible={asking}
|
|
112
|
+
tone="destructive"
|
|
113
|
+
title="Delete this order?"
|
|
114
|
+
message="This cannot be undone."
|
|
115
|
+
confirmLabel="Delete"
|
|
116
|
+
onCancel={() => setAsking(false)}
|
|
117
|
+
onConfirm={async () => {
|
|
118
|
+
try {
|
|
119
|
+
await onDelete(order.id);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
showToast({ kind: "error", message: err.message });
|
|
122
|
+
}
|
|
123
|
+
setAsking(false);
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Props: `visible`, `title` (the question), `message` (what confirming does; text is drawn in the theme's body style, any other node as it is), `confirmLabel` and `cancelLabel`, `tone` (`default` | `destructive`), `busy`, `onConfirm`, `onCancel`, `closeAccessibilityLabel` and `testID` (the parts take `-message`, `-cancel`, `-confirm` and `-busy`, plus the Modal's own).
|
|
132
|
+
|
|
133
|
+
- **Cancelling is the safe default.** The Cancel button, the close button, a backdrop press, Escape on web and the Android back button all fire `onCancel`, never `onConfirm`.
|
|
134
|
+
- **`tone="destructive"`** draws the confirm button as the danger `<Button>`, so it takes its colours from the theme's `destructive` scope (Design > Components > Destructive), and on web opens with focus on Cancel. `default` draws the primary button and opens with focus on Confirm. Focus placement is web only.
|
|
135
|
+
- **Busy.** Return the action's promise from `onConfirm` and the dialog stays busy until it settles: both buttons are disabled, a `<Spinner>` shows, nothing dismisses the dialog, and a second press is ignored. Pass `busy` yourself when the action runs elsewhere; the two combine. A rejected promise is **not** caught: handle the failure inside `onConfirm`, otherwise it is reported as an unhandled rejection. Either way the dialog becomes usable again.
|
|
136
|
+
- **It never closes itself.** Set `visible` to false from `onCancel` and once the action is done.
|
|
137
|
+
- **Labels follow the app's language.** Unset, the buttons read the app's shared `confirm`, `cancel` and `close` translations (the same ones `useI18n().t("cancel")` resolves), falling back to English. Prefer a `confirmLabel` that names the action: "Delete", "Remove", "Send".
|
|
138
|
+
|
|
139
|
+
It adds no theme scope: the panel follows `modal`, the actions `button`, a destructive confirm `destructive`. `<Button>` now forwards a `ref` to its pressable, which is how the dialog places focus. Additive.
|
|
87
140
|
|
|
88
141
|
### What's new in 0.212.0 (contract 1.171.0)
|
|
89
142
|
|
|
@@ -201,7 +254,7 @@ Like the `loading` scope, `alert` styles an SDK component rather than a widget's
|
|
|
201
254
|
|
|
202
255
|
**An app's user totals for its own admins — `useUserStats({ days, group_id })`.** Active and deactivated users, new users per day (7 / 30 days) or week (90 days) with the previous period's total beside them, users in no group, empty groups, and invite figures. Totals only — never a list of people. Needs `users.read:*` and the viewer's admin user access (else `error.code === "FORBIDDEN"`); `stats.invites` is null unless the widget also declares `users.write:*` and the viewer holds it.
|
|
203
256
|
|
|
204
|
-
**User activity — `useUserActivity({ days, group_id })` and `useInactiveUsers({ inactive_days, group_id, limit, offset })`.** For a workspace that records user activity: who is online now, active in the last day / week / month, never active, active users per day or week, the web / iOS / Android split and the active share per group — and a page of the users not seen recently, never-seen first. Needs `users.activity:*` and the viewer's "View user activity" permission (else `error.code === "FORBIDDEN"`); when the workspace does not record activity, `error.reason === "ACTIVITY_DISABLED"` — show that, not zeros. Before user activity is offered to the workspace at all, `error.reason` is `"ACTIVITY_UNAVAILABLE"`: hide the section, as for `FORBIDDEN`. `useInactiveUsers` also returns `remind(userId, { title, body })`, which notifies one of them and records when (each row carries `last_reminded_at`). Days follow the workspace's time zone (`period.time_zone`).
|
|
257
|
+
**User activity — `useUserActivity({ days, group_id })` and `useInactiveUsers({ inactive_days, group_id, limit, offset })`.** For a workspace that records user activity: who is online now, active in the last day / week / month, never active, active users per day or week, the web / iOS / Android split and the active share per group — and a page of the users not seen recently, never-seen first. Needs `users.activity:*` and the viewer's "View user activity" permission (else `error.code === "FORBIDDEN"`); when the workspace does not record activity, `error.reason === "ACTIVITY_DISABLED"` — show that, not zeros. Before user activity is offered to the workspace at all, `error.reason` is `"ACTIVITY_UNAVAILABLE"`: hide the section, as for `FORBIDDEN`. `useInactiveUsers` also returns `remind(userId, { title, body })`, which notifies one of them and records when (each row carries `last_reminded_at`); pass `{ refetch: false }` as a third argument when reminding several, then call `refetch()` once. Days follow the workspace's time zone (`period.time_zone`).
|
|
205
258
|
|
|
206
259
|
**Linter fix.** A widget that uses `useGroups` or `useUsers` is no longer refused for a write it never makes: `StyleSheet.create(...)` and an event subscription's bare `remove()` (an `AppState` listener, say) are not user or group writes.
|
|
207
260
|
|
|
@@ -2550,6 +2603,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2550
2603
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
2551
2604
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
2552
2605
|
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
|
2606
|
+
- `ConfirmDialog` — the standard confirmation prompt, a preset of `Modal` and `Button`. `<ConfirmDialog visible={asking} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel="Delete" onCancel={() => setAsking(false)} onConfirm={removeOrder} />` asks before a delete or any other action that cannot be undone. Cancel, the close button, the backdrop, Escape and the Android back button fire `onCancel`; a promise returned from `onConfirm` keeps it busy (buttons disabled, a second press ignored) until it settles. Props: `visible`, `title`, `message`, `confirmLabel`, `cancelLabel`, `tone` (`default` | `destructive`), `busy`, `onConfirm`, `onCancel`, `closeAccessibilityLabel`, `testID`. Use it instead of building a confirmation from `Modal` yourself.
|
|
2553
2607
|
- `Modal` — the standard dialog, built on `Overlay`. `<Modal visible={open} onRequestClose={() => setOpen(false)} title="Edit order" footer={<Button title="Save" onPress={save} />}>…</Modal>` draws a title, a close button, a body that scrolls when it is long and a footer action row laid out below it. Its background, corner radius, padding, scrim, shadow and title colour come from the theme's `modal` scope (Design > Components > Dialogs), so every dialog in an app matches. Props: `visible`, `onRequestClose`, `dismissible`, `title`, `footer`, `presentation` (`dialog` | `sheet`), `size` (`sm` | `md` | `lg` | `full`), `dismissOnBackdropPress`, `role`, `initialFocusRef`, `closeAccessibilityLabel`, `accessibilityLabel`, `testID`. Use it for every dialog instead of hand-building a header and an action row inside `Overlay`.
|
|
2554
2608
|
- `RichText` — the formatted-content renderer (sc-6970). `<RichText value={post.body} />` parses the markdown subset (`**bold**`, `*italic*` / `_italic_`, `` `code` ``, `#`/`##`/`###` headings, `-`/`*` bullets, `1.` ordered items, one-line `` images) and renders it with SDK primitives, so formatted content reads the same in the web Player and the exported Expo app. `value` is markdown, NOT HTML — widgets have no `dangerouslySetInnerHTML` on either host, so tags in a content string render as literal tag soup; stored text that still holds legacy HTML is stripped to plain text rather than shown as markup. Props: `value`, `renderImage` (`({ src, alt, size }) => node` — supply it when images are filestore ids, because resolving one needs the scopes your own widget holds; without it only absolute `http(s)` URLs render), `style`, `testID`. Colour, type scale and leading come from the workspace theme — never re-style them.
|
|
2555
2609
|
- `MarkdownInput` — the authoring half of `<RichText>` (sc-6970). `<MarkdownInput value={draft} onChange={setDraft} />` is a multi-line field, a toolbar whose buttons wrap the current SELECTION in markdown markers (bold, italic, code, H2, H3, bullets, numbered list, each toggling back off), and a live `<RichText>` preview of the result underneath — the way to let an app USER write formatted text, instead of a bare `TextInput` plus hand-rolled buttons that can only splice literal tags into the value. Props: `value`, `onChange(next)`, `placeholder`, `previewLabel` (default `Preview`), `showPreview` (default true), `renderImage` (passed through to the preview), `minHeight` (150) and `maxHeight` (280) so a long draft scrolls inside its box, `accessibilityLabel`, `style`, `testID`.
|
package/dist/button-view.js
CHANGED
|
@@ -100,8 +100,9 @@ export function makeButton(rn) {
|
|
|
100
100
|
* @param {boolean} [props.hostThemed] — `fields` already carry the theme
|
|
101
101
|
* (a built-in the host folded); skip the second fold, which would add the
|
|
102
102
|
* Button widget's own scopes to a form submit or a Link action.
|
|
103
|
+
* @param {object} ref — forwarded to the pressable, so a caller can focus it.
|
|
103
104
|
*/
|
|
104
|
-
function Button({
|
|
105
|
+
const Button = React.forwardRef(function Button({
|
|
105
106
|
title,
|
|
106
107
|
variant = "primary",
|
|
107
108
|
icon,
|
|
@@ -114,7 +115,7 @@ export function makeButton(rn) {
|
|
|
114
115
|
hostThemed = false,
|
|
115
116
|
accessibilityLabel,
|
|
116
117
|
testID,
|
|
117
|
-
}) {
|
|
118
|
+
}, ref) {
|
|
118
119
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
119
120
|
const { colors, spacing, radii, typography } = theme;
|
|
120
121
|
const f = hostThemed
|
|
@@ -192,6 +193,7 @@ export function makeButton(rn) {
|
|
|
192
193
|
return React.createElement(
|
|
193
194
|
Pressable,
|
|
194
195
|
{
|
|
196
|
+
ref,
|
|
195
197
|
onPress,
|
|
196
198
|
disabled,
|
|
197
199
|
accessibilityRole: "button",
|
|
@@ -202,7 +204,7 @@ export function makeButton(rn) {
|
|
|
202
204
|
},
|
|
203
205
|
children,
|
|
204
206
|
);
|
|
205
|
-
}
|
|
207
|
+
});
|
|
206
208
|
|
|
207
209
|
Button.displayName = "Button";
|
|
208
210
|
return Button;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// The ONE implementation behind the `<ConfirmDialog>` SDK component: a preset
|
|
2
|
+
// of `<Modal>` holding a message, a Cancel and a Confirm `<Button>`.
|
|
3
|
+
//
|
|
4
|
+
// Bound per host by confirm-dialog.js / confirm-dialog.native.js, which differ
|
|
5
|
+
// only in where the primitives come from, so the two hosts cannot drift.
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { useHostSharedText, useHostTheme } from "./hooks.js";
|
|
9
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
10
|
+
import { textRoleStyle } from "./text-roles.js";
|
|
11
|
+
|
|
12
|
+
function isThenable(value) {
|
|
13
|
+
return value != null && typeof value.then === "function";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isText(value) {
|
|
17
|
+
return typeof value === "string" || typeof value === "number";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Bind the shared confirmation to one platform's primitives and to that
|
|
22
|
+
* host's `<Modal>`, `<Button>` and `<Spinner>`.
|
|
23
|
+
*
|
|
24
|
+
* @param {{Text: any}} rn
|
|
25
|
+
* @param {any} Modal — the host's bound `<Modal>`.
|
|
26
|
+
* @param {any} Button — the host's bound `<Button>`.
|
|
27
|
+
* @param {any} Spinner — the host's bound `<Spinner>`.
|
|
28
|
+
*/
|
|
29
|
+
export function makeConfirmDialog(rn, Modal, Button, Spinner) {
|
|
30
|
+
const { Text } = rn;
|
|
31
|
+
|
|
32
|
+
// Rendered inside the Modal, so it reads the theme the Modal re-inks.
|
|
33
|
+
function Message({ testID, children }) {
|
|
34
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
35
|
+
return React.createElement(
|
|
36
|
+
Text,
|
|
37
|
+
{ testID, style: textRoleStyle("body", theme.typography, theme.colors) },
|
|
38
|
+
children,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The standard "are you sure?" prompt. Cancelling is the safe default: the
|
|
44
|
+
* close button, the backdrop, Escape (web) and the Android back button all
|
|
45
|
+
* fire `onCancel`, never `onConfirm`.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} props
|
|
48
|
+
* @param {boolean} [props.visible] — nothing is mounted while false.
|
|
49
|
+
* @param {string} props.title — the question, e.g. "Delete this order?".
|
|
50
|
+
* @param {any} [props.message] — what confirming does; text is drawn in the
|
|
51
|
+
* theme's body style, any other node is rendered as it is.
|
|
52
|
+
* @param {string} [props.confirmLabel] — default the app's "Confirm".
|
|
53
|
+
* @param {string} [props.cancelLabel] — default the app's "Cancel".
|
|
54
|
+
* @param {"default"|"destructive"} [props.tone] — `destructive` draws the
|
|
55
|
+
* confirm button from the theme's Destructive colours and, on web, opens
|
|
56
|
+
* with focus on Cancel; `default` opens with focus on Confirm.
|
|
57
|
+
* @param {boolean} [props.busy] — the confirmed action is running: both
|
|
58
|
+
* buttons are disabled, a loading indicator shows, nothing closes it.
|
|
59
|
+
* @param {() => void|Promise<any>} [props.onConfirm] — a returned promise
|
|
60
|
+
* keeps the dialog busy until it settles. A rejection is not caught.
|
|
61
|
+
* @param {() => void} [props.onCancel]
|
|
62
|
+
* @param {string} [props.closeAccessibilityLabel] — default the app's "Close".
|
|
63
|
+
* @param {string} [props.testID] — on the panel; the parts take `-message`,
|
|
64
|
+
* `-cancel`, `-confirm` and `-busy`, plus the Modal's own.
|
|
65
|
+
*/
|
|
66
|
+
function ConfirmDialog({
|
|
67
|
+
visible = false,
|
|
68
|
+
title,
|
|
69
|
+
message,
|
|
70
|
+
confirmLabel,
|
|
71
|
+
cancelLabel,
|
|
72
|
+
tone = "default",
|
|
73
|
+
busy = false,
|
|
74
|
+
onConfirm,
|
|
75
|
+
onCancel,
|
|
76
|
+
closeAccessibilityLabel,
|
|
77
|
+
testID,
|
|
78
|
+
}) {
|
|
79
|
+
const sharedConfirm = useHostSharedText("confirm", "Confirm");
|
|
80
|
+
const sharedCancel = useHostSharedText("cancel", "Cancel");
|
|
81
|
+
const sharedClose = useHostSharedText("close", "Close");
|
|
82
|
+
const [pending, setPending] = React.useState(false);
|
|
83
|
+
// Set before the re-render, so a second press in the same frame is ignored.
|
|
84
|
+
const inFlight = React.useRef(false);
|
|
85
|
+
const cancelRef = React.useRef(null);
|
|
86
|
+
const confirmRef = React.useRef(null);
|
|
87
|
+
|
|
88
|
+
const working = busy || pending;
|
|
89
|
+
const destructive = tone === "destructive";
|
|
90
|
+
const part = (name) => (testID ? `${testID}-${name}` : undefined);
|
|
91
|
+
|
|
92
|
+
const confirm = () => {
|
|
93
|
+
if (working || inFlight.current || typeof onConfirm !== "function") return;
|
|
94
|
+
const result = onConfirm();
|
|
95
|
+
if (!isThenable(result)) return;
|
|
96
|
+
inFlight.current = true;
|
|
97
|
+
setPending(true);
|
|
98
|
+
const settle = () => {
|
|
99
|
+
inFlight.current = false;
|
|
100
|
+
setPending(false);
|
|
101
|
+
};
|
|
102
|
+
result.then(settle, (error) => {
|
|
103
|
+
settle();
|
|
104
|
+
// Rethrown so a failure the caller did not handle is still reported.
|
|
105
|
+
throw error;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const cancel = () => {
|
|
110
|
+
if (working || inFlight.current) return;
|
|
111
|
+
if (typeof onCancel === "function") onCancel();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const footer = React.createElement(
|
|
115
|
+
React.Fragment,
|
|
116
|
+
null,
|
|
117
|
+
working ? React.createElement(Spinner, { testID: part("busy") }) : null,
|
|
118
|
+
React.createElement(Button, {
|
|
119
|
+
ref: cancelRef,
|
|
120
|
+
variant: "secondary",
|
|
121
|
+
title: cancelLabel ?? sharedCancel,
|
|
122
|
+
onPress: cancel,
|
|
123
|
+
disabled: working,
|
|
124
|
+
testID: part("cancel"),
|
|
125
|
+
}),
|
|
126
|
+
React.createElement(Button, {
|
|
127
|
+
ref: confirmRef,
|
|
128
|
+
variant: destructive ? "danger" : "primary",
|
|
129
|
+
title: confirmLabel ?? sharedConfirm,
|
|
130
|
+
onPress: confirm,
|
|
131
|
+
disabled: working,
|
|
132
|
+
testID: part("confirm"),
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return React.createElement(
|
|
137
|
+
Modal,
|
|
138
|
+
{
|
|
139
|
+
visible,
|
|
140
|
+
title,
|
|
141
|
+
footer,
|
|
142
|
+
role: "alertdialog",
|
|
143
|
+
size: "sm",
|
|
144
|
+
dismissible: !working,
|
|
145
|
+
onRequestClose: cancel,
|
|
146
|
+
initialFocusRef: destructive ? cancelRef : confirmRef,
|
|
147
|
+
closeAccessibilityLabel: closeAccessibilityLabel ?? sharedClose,
|
|
148
|
+
testID,
|
|
149
|
+
},
|
|
150
|
+
isText(message)
|
|
151
|
+
? React.createElement(Message, { testID: part("message") }, message)
|
|
152
|
+
: message,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ConfirmDialog.displayName = "ConfirmDialog";
|
|
157
|
+
return ConfirmDialog;
|
|
158
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `<ConfirmDialog>` (web). Binds ./confirm-dialog-view.js to react-native-web
|
|
2
|
+
// and the web `<Modal>`, `<Button>` and `<Spinner>`; see ./primitives.js for
|
|
3
|
+
// why not the bare specifier.
|
|
4
|
+
|
|
5
|
+
import { Text } from "react-native-web";
|
|
6
|
+
import { makeConfirmDialog } from "./confirm-dialog-view.js";
|
|
7
|
+
import { Modal } from "./modal.js";
|
|
8
|
+
import { Button } from "./button.js";
|
|
9
|
+
import { Spinner } from "./loading.js";
|
|
10
|
+
|
|
11
|
+
export const ConfirmDialog = makeConfirmDialog({ Text }, Modal, Button, Spinner);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// `<ConfirmDialog>` (native). Binds ./confirm-dialog-view.js to React Native's
|
|
2
|
+
// own primitives and the native `<Modal>`, `<Button>` and `<Spinner>`; only the
|
|
3
|
+
// imports differ from ./confirm-dialog.js.
|
|
4
|
+
|
|
5
|
+
import { Text } from "react-native";
|
|
6
|
+
import { makeConfirmDialog } from "./confirm-dialog-view.js";
|
|
7
|
+
import { Modal } from "./modal.native.js";
|
|
8
|
+
import { Button } from "./button.native.js";
|
|
9
|
+
import { Spinner } from "./loading.native.js";
|
|
10
|
+
|
|
11
|
+
export const ConfirmDialog = makeConfirmDialog({ Text }, Modal, Button, Spinner);
|
package/dist/contract.cjs
CHANGED
|
@@ -1960,7 +1960,9 @@ const HOOKS = [
|
|
|
1960
1960
|
"Returns { users, loading, error, refetch, invite, deactivate, " +
|
|
1961
1961
|
"reactivate, remove, sendPasswordReset, update }. " +
|
|
1962
1962
|
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1963
|
-
"res.data; rows are snake_case (is_active, …).
|
|
1963
|
+
"res.data; rows are snake_case (is_active, created_at, …). For an admin, " +
|
|
1964
|
+
"query { joined_since, sort: \"newest\" } lists who joined since a date " +
|
|
1965
|
+
"(read in the workspace's time zone), newest first. Reads need users.read:* " +
|
|
1964
1966
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1965
1967
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
1966
1968
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
@@ -2082,6 +2084,30 @@ const HOOKS = [
|
|
|
2082
2084
|
requiredContextSlice: ["directory.users"],
|
|
2083
2085
|
scopes: ["users.activity:*"],
|
|
2084
2086
|
},
|
|
2087
|
+
// sc-11066 — who was seen in a window, behind users.activity.
|
|
2088
|
+
{
|
|
2089
|
+
name: "useActiveUsers",
|
|
2090
|
+
signature: "useActiveUsers({ within, group_id, limit, offset } | null)",
|
|
2091
|
+
optionalArgs: ["within", "group_id", "limit", "offset"],
|
|
2092
|
+
description:
|
|
2093
|
+
"One page of users seen within `within`: \"online\" (default; the app " +
|
|
2094
|
+
"open in the last two minutes), \"day\" (since midnight in the " +
|
|
2095
|
+
"workspace's time zone), \"week\" or \"month\" (the last 7 / 30 days), " +
|
|
2096
|
+
"most recently seen first: `users` = [{ id, name, last_active_at, " +
|
|
2097
|
+
"last_platform }], `total` for paging. Use it to show WHO is behind an " +
|
|
2098
|
+
"online or active figure. Same gate and errors as useUserActivity; null " +
|
|
2099
|
+
"reads nothing.",
|
|
2100
|
+
returnShape: {
|
|
2101
|
+
users: "Array<{ id, name, last_active_at, last_platform }>",
|
|
2102
|
+
total: "number | null",
|
|
2103
|
+
loading: "boolean",
|
|
2104
|
+
fetching: "boolean",
|
|
2105
|
+
error: "DirectoryError | null",
|
|
2106
|
+
refetch: "() => Promise<void>",
|
|
2107
|
+
},
|
|
2108
|
+
requiredContextSlice: ["directory.users"],
|
|
2109
|
+
scopes: ["users.activity:*"],
|
|
2110
|
+
},
|
|
2085
2111
|
{
|
|
2086
2112
|
name: "useInactiveUsers",
|
|
2087
2113
|
signature: "useInactiveUsers({ inactive_days, group_id, limit, offset } | null)",
|
|
@@ -2091,7 +2117,8 @@ const HOOKS = [
|
|
|
2091
2117
|
"never-seen first, then least recently seen: `users` = [{ id, name, " +
|
|
2092
2118
|
"last_active_at, last_reminded_at }] (last_active_at null = never seen " +
|
|
2093
2119
|
"since recording began), `total` for paging. `remind(id, { title, body })` " +
|
|
2094
|
-
"notifies one of them, records when (last_reminded_at) and re-reads
|
|
2120
|
+
"notifies one of them, records when (last_reminded_at) and re-reads; " +
|
|
2121
|
+
"pass `{ refetch: false }` third to skip the re-read in a batch. " +
|
|
2095
2122
|
"Same gate and errors as useUserActivity; null reads nothing.",
|
|
2096
2123
|
returnShape: {
|
|
2097
2124
|
users: "Array<{ id, name, last_active_at, last_reminded_at }>",
|
|
@@ -2100,7 +2127,7 @@ const HOOKS = [
|
|
|
2100
2127
|
fetching: "boolean",
|
|
2101
2128
|
error: "DirectoryError | null",
|
|
2102
2129
|
refetch: "() => Promise<void>",
|
|
2103
|
-
remind: "(id, { title, body }) => Promise<void>",
|
|
2130
|
+
remind: "(id, { title, body }, { refetch? }) => Promise<void>",
|
|
2104
2131
|
},
|
|
2105
2132
|
requiredContextSlice: ["directory.users"],
|
|
2106
2133
|
scopes: ["users.activity:*"],
|
|
@@ -2920,6 +2947,13 @@ const PRIMITIVES = [
|
|
|
2920
2947
|
rnComponent: null,
|
|
2921
2948
|
docsUrl: null,
|
|
2922
2949
|
},
|
|
2950
|
+
{
|
|
2951
|
+
name: "ConfirmDialog",
|
|
2952
|
+
description:
|
|
2953
|
+
'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
|
|
2954
|
+
rnComponent: null,
|
|
2955
|
+
docsUrl: null,
|
|
2956
|
+
},
|
|
2923
2957
|
// sc-8104 — the in-app emoji picker. Typed emoji always worked; what this
|
|
2924
2958
|
// replaces is being forced out to the OS keyboard to find one.
|
|
2925
2959
|
{
|
|
@@ -5628,7 +5662,16 @@ const CONTRACT = deepFreeze({
|
|
|
5628
5662
|
// switch, so an existing `<Switch>` with no colour props is repainted:
|
|
5629
5663
|
// its tracks follow the theme on both hosts and its "on" thumb is white
|
|
5630
5664
|
// on the web. A caller's `trackColor` / `thumbColor` still win.
|
|
5631
|
-
|
|
5665
|
+
// 1.172.0: additive (sc-10925) — the `ConfirmDialog` primitive: the standard
|
|
5666
|
+
// confirmation, a preset of `Modal` and `Button` (title, message, confirm
|
|
5667
|
+
// and cancel labels, `tone` "default" | "destructive", `busy`). No new
|
|
5668
|
+
// theme scope: the panel reads `modal`, the actions `button` and
|
|
5669
|
+
// `destructive`. `Button` forwards a ref to its pressable.
|
|
5670
|
+
// 1.173.0: additive (sc-11066) — `useActiveUsers(query)` lists who was
|
|
5671
|
+
// seen online / today / in 7 or 30 days through the new
|
|
5672
|
+
// `ctx.directory.users.active`; `useUsers` takes `joined_since` and
|
|
5673
|
+
// `sort: "newest"` for an admin's "who joined" list.
|
|
5674
|
+
version: "1.173.0",
|
|
5632
5675
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5633
5676
|
hooks: HOOKS,
|
|
5634
5677
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1960,7 +1960,9 @@ const HOOKS = [
|
|
|
1960
1960
|
"Returns { users, loading, error, refetch, invite, deactivate, " +
|
|
1961
1961
|
"reactivate, remove, sendPasswordReset, update }. " +
|
|
1962
1962
|
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1963
|
-
"res.data; rows are snake_case (is_active, …).
|
|
1963
|
+
"res.data; rows are snake_case (is_active, created_at, …). For an admin, " +
|
|
1964
|
+
"query { joined_since, sort: \"newest\" } lists who joined since a date " +
|
|
1965
|
+
"(read in the workspace's time zone), newest first. Reads need users.read:* " +
|
|
1964
1966
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1965
1967
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
1966
1968
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
@@ -2082,6 +2084,30 @@ const HOOKS = [
|
|
|
2082
2084
|
requiredContextSlice: ["directory.users"],
|
|
2083
2085
|
scopes: ["users.activity:*"],
|
|
2084
2086
|
},
|
|
2087
|
+
// sc-11066 — who was seen in a window, behind users.activity.
|
|
2088
|
+
{
|
|
2089
|
+
name: "useActiveUsers",
|
|
2090
|
+
signature: "useActiveUsers({ within, group_id, limit, offset } | null)",
|
|
2091
|
+
optionalArgs: ["within", "group_id", "limit", "offset"],
|
|
2092
|
+
description:
|
|
2093
|
+
"One page of users seen within `within`: \"online\" (default; the app " +
|
|
2094
|
+
"open in the last two minutes), \"day\" (since midnight in the " +
|
|
2095
|
+
"workspace's time zone), \"week\" or \"month\" (the last 7 / 30 days), " +
|
|
2096
|
+
"most recently seen first: `users` = [{ id, name, last_active_at, " +
|
|
2097
|
+
"last_platform }], `total` for paging. Use it to show WHO is behind an " +
|
|
2098
|
+
"online or active figure. Same gate and errors as useUserActivity; null " +
|
|
2099
|
+
"reads nothing.",
|
|
2100
|
+
returnShape: {
|
|
2101
|
+
users: "Array<{ id, name, last_active_at, last_platform }>",
|
|
2102
|
+
total: "number | null",
|
|
2103
|
+
loading: "boolean",
|
|
2104
|
+
fetching: "boolean",
|
|
2105
|
+
error: "DirectoryError | null",
|
|
2106
|
+
refetch: "() => Promise<void>",
|
|
2107
|
+
},
|
|
2108
|
+
requiredContextSlice: ["directory.users"],
|
|
2109
|
+
scopes: ["users.activity:*"],
|
|
2110
|
+
},
|
|
2085
2111
|
{
|
|
2086
2112
|
name: "useInactiveUsers",
|
|
2087
2113
|
signature: "useInactiveUsers({ inactive_days, group_id, limit, offset } | null)",
|
|
@@ -2091,7 +2117,8 @@ const HOOKS = [
|
|
|
2091
2117
|
"never-seen first, then least recently seen: `users` = [{ id, name, " +
|
|
2092
2118
|
"last_active_at, last_reminded_at }] (last_active_at null = never seen " +
|
|
2093
2119
|
"since recording began), `total` for paging. `remind(id, { title, body })` " +
|
|
2094
|
-
"notifies one of them, records when (last_reminded_at) and re-reads
|
|
2120
|
+
"notifies one of them, records when (last_reminded_at) and re-reads; " +
|
|
2121
|
+
"pass `{ refetch: false }` third to skip the re-read in a batch. " +
|
|
2095
2122
|
"Same gate and errors as useUserActivity; null reads nothing.",
|
|
2096
2123
|
returnShape: {
|
|
2097
2124
|
users: "Array<{ id, name, last_active_at, last_reminded_at }>",
|
|
@@ -2100,7 +2127,7 @@ const HOOKS = [
|
|
|
2100
2127
|
fetching: "boolean",
|
|
2101
2128
|
error: "DirectoryError | null",
|
|
2102
2129
|
refetch: "() => Promise<void>",
|
|
2103
|
-
remind: "(id, { title, body }) => Promise<void>",
|
|
2130
|
+
remind: "(id, { title, body }, { refetch? }) => Promise<void>",
|
|
2104
2131
|
},
|
|
2105
2132
|
requiredContextSlice: ["directory.users"],
|
|
2106
2133
|
scopes: ["users.activity:*"],
|
|
@@ -2920,6 +2947,13 @@ const PRIMITIVES = [
|
|
|
2920
2947
|
rnComponent: null,
|
|
2921
2948
|
docsUrl: null,
|
|
2922
2949
|
},
|
|
2950
|
+
{
|
|
2951
|
+
name: "ConfirmDialog",
|
|
2952
|
+
description:
|
|
2953
|
+
'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
|
|
2954
|
+
rnComponent: null,
|
|
2955
|
+
docsUrl: null,
|
|
2956
|
+
},
|
|
2923
2957
|
// sc-8104 — the in-app emoji picker. Typed emoji always worked; what this
|
|
2924
2958
|
// replaces is being forced out to the OS keyboard to find one.
|
|
2925
2959
|
{
|
|
@@ -5624,7 +5658,16 @@ const CONTRACT = deepFreeze({
|
|
|
5624
5658
|
// switch, so an existing `<Switch>` with no colour props is repainted:
|
|
5625
5659
|
// its tracks follow the theme on both hosts and its "on" thumb is white
|
|
5626
5660
|
// on the web. A caller's `trackColor` / `thumbColor` still win.
|
|
5627
|
-
|
|
5661
|
+
// 1.172.0: additive (sc-10925) — the `ConfirmDialog` primitive: the standard
|
|
5662
|
+
// confirmation, a preset of `Modal` and `Button` (title, message, confirm
|
|
5663
|
+
// and cancel labels, `tone` "default" | "destructive", `busy`). No new
|
|
5664
|
+
// theme scope: the panel reads `modal`, the actions `button` and
|
|
5665
|
+
// `destructive`. `Button` forwards a ref to its pressable.
|
|
5666
|
+
// 1.173.0: additive (sc-11066) — `useActiveUsers(query)` lists who was
|
|
5667
|
+
// seen online / today / in 7 or 30 days through the new
|
|
5668
|
+
// `ctx.directory.users.active`; `useUsers` takes `joined_since` and
|
|
5669
|
+
// `sort: "newest"` for an admin's "who joined" list.
|
|
5670
|
+
version: "1.173.0",
|
|
5628
5671
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5629
5672
|
hooks: HOOKS,
|
|
5630
5673
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -1107,6 +1107,21 @@ export function useUserActivity(query?: { days?: 7 | 30 | 90; group_id?: string
|
|
|
1107
1107
|
refetch(): Promise<void>;
|
|
1108
1108
|
};
|
|
1109
1109
|
|
|
1110
|
+
/** Users seen within `within` (default "online"), most recently seen first. */
|
|
1111
|
+
export function useActiveUsers(query?: {
|
|
1112
|
+
within?: "online" | "day" | "week" | "month";
|
|
1113
|
+
group_id?: string;
|
|
1114
|
+
limit?: number;
|
|
1115
|
+
offset?: number;
|
|
1116
|
+
} | null): {
|
|
1117
|
+
users: Array<{ id: string; name: string; last_active_at: string; last_platform: "web" | "ios" | "android" | "unknown" }>;
|
|
1118
|
+
total: number | null;
|
|
1119
|
+
loading: boolean;
|
|
1120
|
+
fetching: boolean;
|
|
1121
|
+
error: Error | null;
|
|
1122
|
+
refetch(): Promise<void>;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1110
1125
|
/** Users not seen for `inactive_days` (default 30), never-seen first. */
|
|
1111
1126
|
export function useInactiveUsers(query?: {
|
|
1112
1127
|
inactive_days?: number;
|
|
@@ -1121,7 +1136,8 @@ export function useInactiveUsers(query?: {
|
|
|
1121
1136
|
error: Error | null;
|
|
1122
1137
|
refetch(): Promise<void>;
|
|
1123
1138
|
/** Notify one user, record when, then re-read the page. */
|
|
1124
|
-
|
|
1139
|
+
/** `{ refetch: false }` skips re-reading the page, for a batch that reads once. */
|
|
1140
|
+
remind(id: string, message: { title: string; body: string }, options?: { refetch?: boolean }): Promise<void>;
|
|
1125
1141
|
};
|
|
1126
1142
|
|
|
1127
1143
|
/** One record's online members, as `useOnlineMembers` reports them. */
|
|
@@ -2743,6 +2759,12 @@ export interface UsersQuery {
|
|
|
2743
2759
|
is_active?: boolean;
|
|
2744
2760
|
/** Restrict to members of one app-user group — the `id` of a `useGroups()` row. */
|
|
2745
2761
|
group_id?: string;
|
|
2762
|
+
/** Admin view only: users created since this date (workspace time zone) or instant. */
|
|
2763
|
+
joined_since?: string;
|
|
2764
|
+
/** Admin view only: "newest" lists the most recently created first. */
|
|
2765
|
+
sort?: "name" | "newest";
|
|
2766
|
+
/** "real" leaves out preview and store-review accounts; an in-app admin always gets "real". */
|
|
2767
|
+
audience?: "all" | "real" | "preview";
|
|
2746
2768
|
limit?: number;
|
|
2747
2769
|
offset?: number;
|
|
2748
2770
|
}
|
|
@@ -3360,6 +3382,70 @@ export const Modal: (props: {
|
|
|
3360
3382
|
children?: ReactNode;
|
|
3361
3383
|
}) => any;
|
|
3362
3384
|
|
|
3385
|
+
export type ConfirmDialogTone = "default" | "destructive";
|
|
3386
|
+
|
|
3387
|
+
/**
|
|
3388
|
+
* The standard confirmation prompt: a preset of `<Modal>` with a message, a
|
|
3389
|
+
* Cancel and a Confirm `<Button>`. Use it before a delete or any other action
|
|
3390
|
+
* that cannot be undone, so every confirmation in the app looks and behaves
|
|
3391
|
+
* the same. Cancelling is the safe default: the Cancel button, the close
|
|
3392
|
+
* button, the backdrop, Escape (web) and the Android back button all fire
|
|
3393
|
+
* `onCancel`, never `onConfirm`.
|
|
3394
|
+
*
|
|
3395
|
+
* It adds no theme scope: the panel follows `modal`, the actions `button`,
|
|
3396
|
+
* and a destructive confirm the `destructive` scope. It never closes itself;
|
|
3397
|
+
* set `visible` false from `onCancel` and once the action is done.
|
|
3398
|
+
*
|
|
3399
|
+
* @example
|
|
3400
|
+
* <ConfirmDialog
|
|
3401
|
+
* visible={!!pending}
|
|
3402
|
+
* tone="destructive"
|
|
3403
|
+
* title="Delete this order?"
|
|
3404
|
+
* message="This cannot be undone."
|
|
3405
|
+
* confirmLabel="Delete"
|
|
3406
|
+
* onCancel={() => setPending(null)}
|
|
3407
|
+
* onConfirm={async () => {
|
|
3408
|
+
* await remove(pending.id);
|
|
3409
|
+
* setPending(null);
|
|
3410
|
+
* }}
|
|
3411
|
+
* />
|
|
3412
|
+
*/
|
|
3413
|
+
export const ConfirmDialog: (props: {
|
|
3414
|
+
/** Nothing renders (and nothing is mounted) while this is false. */
|
|
3415
|
+
visible?: boolean;
|
|
3416
|
+
/** The question, e.g. "Delete this order?". */
|
|
3417
|
+
title: string;
|
|
3418
|
+
/** What confirming does. Text is drawn in the theme's body style. */
|
|
3419
|
+
message?: ReactNode;
|
|
3420
|
+
/** Defaults to the app's translated "Confirm". Name the action instead. */
|
|
3421
|
+
confirmLabel?: string;
|
|
3422
|
+
/** Defaults to the app's translated "Cancel". */
|
|
3423
|
+
cancelLabel?: string;
|
|
3424
|
+
/**
|
|
3425
|
+
* `destructive` draws the confirm button from the theme's Destructive
|
|
3426
|
+
* colours and, on web, opens with focus on Cancel. `default` opens with
|
|
3427
|
+
* focus on Confirm. Focus placement is web only.
|
|
3428
|
+
*/
|
|
3429
|
+
tone?: ConfirmDialogTone;
|
|
3430
|
+
/**
|
|
3431
|
+
* The confirmed action is running: both buttons are disabled, a loading
|
|
3432
|
+
* indicator shows, and the backdrop, Escape and Android back do nothing.
|
|
3433
|
+
*/
|
|
3434
|
+
busy?: boolean;
|
|
3435
|
+
/**
|
|
3436
|
+
* Return the action's promise and the dialog stays busy until it settles,
|
|
3437
|
+
* ignoring a second press. A rejection is not caught: handle the failure
|
|
3438
|
+
* here, or it is reported as an unhandled rejection.
|
|
3439
|
+
*/
|
|
3440
|
+
onConfirm?: () => void | Promise<unknown>;
|
|
3441
|
+
/** The Cancel and close buttons, backdrop, Escape (web) and Android back. */
|
|
3442
|
+
onCancel?: () => void;
|
|
3443
|
+
/** Label for the close button. Defaults to the app's translated "Close". */
|
|
3444
|
+
closeAccessibilityLabel?: string;
|
|
3445
|
+
/** On the panel; the parts take `-message`, `-cancel`, `-confirm` and `-busy`. */
|
|
3446
|
+
testID?: string;
|
|
3447
|
+
}) => any;
|
|
3448
|
+
|
|
3363
3449
|
/** Per-list row overrides; unset values follow Theme Settings > Lists. */
|
|
3364
3450
|
export interface ListRowTokens {
|
|
3365
3451
|
rowPadding?: number;
|
|
@@ -3616,6 +3702,8 @@ export const Button: (props: {
|
|
|
3616
3702
|
labelStyle?: any;
|
|
3617
3703
|
accessibilityLabel?: string;
|
|
3618
3704
|
testID?: string;
|
|
3705
|
+
/** Forwarded to the pressable, so you can focus the button. */
|
|
3706
|
+
ref?: { current: any } | ((node: any) => void);
|
|
3619
3707
|
}) => any;
|
|
3620
3708
|
|
|
3621
3709
|
/**
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,7 @@ export { useDatastoreFigure } from "./datastore-figure.js";
|
|
|
127
127
|
export {
|
|
128
128
|
useUserStats,
|
|
129
129
|
useUserActivity,
|
|
130
|
+
useActiveUsers,
|
|
130
131
|
useInactiveUsers,
|
|
131
132
|
} from "./stats-hooks.js";
|
|
132
133
|
// sc-11025 — one source, bound to how each platform says the app is in front.
|
|
@@ -282,6 +283,8 @@ export { Spinner, Skeleton } from "./loading.js";
|
|
|
282
283
|
export { Banner } from "./banner.js";
|
|
283
284
|
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
284
285
|
export { Modal } from "./modal.js";
|
|
286
|
+
// The standard confirmation, a preset of `<Modal>` and `<Button>`.
|
|
287
|
+
export { ConfirmDialog } from "./confirm-dialog.js";
|
|
285
288
|
// The semantic text tiers, shared with the built-in widgets.
|
|
286
289
|
export {
|
|
287
290
|
textRoleStyle,
|
package/dist/index.native.js
CHANGED
|
@@ -125,6 +125,7 @@ export { useDatastoreFigure } from "./datastore-figure.js";
|
|
|
125
125
|
export {
|
|
126
126
|
useUserStats,
|
|
127
127
|
useUserActivity,
|
|
128
|
+
useActiveUsers,
|
|
128
129
|
useInactiveUsers,
|
|
129
130
|
} from "./stats-hooks.js";
|
|
130
131
|
// sc-11025 — see the note on the web mirror in ./index.js.
|
|
@@ -275,6 +276,8 @@ export { Spinner, Skeleton } from "./loading.native.js";
|
|
|
275
276
|
export { Banner } from "./banner.native.js";
|
|
276
277
|
// The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
|
|
277
278
|
export { Modal } from "./modal.native.js";
|
|
279
|
+
// The standard confirmation, a preset of `<Modal>` and `<Button>`.
|
|
280
|
+
export { ConfirmDialog } from "./confirm-dialog.native.js";
|
|
278
281
|
// The semantic text tiers, shared with the built-in widgets.
|
|
279
282
|
export {
|
|
280
283
|
textRoleStyle,
|
package/dist/stats-hooks.js
CHANGED
|
@@ -108,13 +108,32 @@ export function useUserActivity(query) {
|
|
|
108
108
|
return { ...state, activity: data };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* sc-11066 — one page of users seen within `within` ("online" — the last two
|
|
113
|
+
* minutes — "day", "week" or "month"), most recently seen first. `query`:
|
|
114
|
+
* `{ within?, group_id?, limit?, offset? }`. Returns `{ users, total, loading,
|
|
115
|
+
* fetching, error, refetch }`; each user is `{ id, name, last_active_at,
|
|
116
|
+
* last_platform }`. Same gate as useUserActivity; null reads nothing.
|
|
117
|
+
*/
|
|
118
|
+
export function useActiveUsers(query) {
|
|
119
|
+
const users = useUsersNamespace("useActiveUsers", "active");
|
|
120
|
+
const key = query === null ? null : serialize(query);
|
|
121
|
+
const { data, ...state } = useKeyedRead(() => users.active(query), "active-users", key, toDirectoryError);
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
users: data && Array.isArray(data.data) ? data.data : [],
|
|
125
|
+
total: data && data.meta && Number.isFinite(data.meta.total) ? data.meta.total : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
/**
|
|
112
130
|
* sc-10691 — one page of users not seen for `inactive_days` (default 30),
|
|
113
131
|
* never-seen first. `query`: `{ inactive_days?, group_id?, limit?, offset? }`.
|
|
114
132
|
* Returns `{ users, total, loading, fetching, error, refetch, remind }`; each
|
|
115
133
|
* user is `{ id, name, last_active_at, last_reminded_at }` (last_active_at
|
|
116
134
|
* null when never seen). `remind(id, { title, body })` notifies one and
|
|
117
|
-
* re-reads the page
|
|
135
|
+
* re-reads the page; `{ refetch: false }` as a third argument skips the
|
|
136
|
+
* re-read, so a batch reads once at the end. Same gate as useUserActivity.
|
|
118
137
|
*/
|
|
119
138
|
export function useInactiveUsers(query) {
|
|
120
139
|
const users = useUsersNamespace("useInactiveUsers", "inactive");
|
|
@@ -122,7 +141,7 @@ export function useInactiveUsers(query) {
|
|
|
122
141
|
const { data, ...state } = useKeyedRead(() => users.inactive(query), "inactive", key, toDirectoryError);
|
|
123
142
|
const { refetch } = state;
|
|
124
143
|
const remind = useCallback(
|
|
125
|
-
async (id, message) => {
|
|
144
|
+
async (id, message, { refetch: reread = true } = {}) => {
|
|
126
145
|
if (typeof users.remind !== "function") {
|
|
127
146
|
throw new Error("useInactiveUsers: host did not inject ctx.directory.users.remind");
|
|
128
147
|
}
|
|
@@ -131,7 +150,7 @@ export function useInactiveUsers(query) {
|
|
|
131
150
|
} catch (err) {
|
|
132
151
|
throw toDirectoryError(err);
|
|
133
152
|
}
|
|
134
|
-
await refetch();
|
|
153
|
+
if (reread) await refetch();
|
|
135
154
|
},
|
|
136
155
|
[users, refetch],
|
|
137
156
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.214.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
|
@@ -1960,7 +1960,9 @@ const HOOKS = [
|
|
|
1960
1960
|
"Returns { users, loading, error, refetch, invite, deactivate, " +
|
|
1961
1961
|
"reactivate, remove, sendPasswordReset, update }. " +
|
|
1962
1962
|
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1963
|
-
"res.data; rows are snake_case (is_active, …).
|
|
1963
|
+
"res.data; rows are snake_case (is_active, created_at, …). For an admin, " +
|
|
1964
|
+
"query { joined_since, sort: \"newest\" } lists who joined since a date " +
|
|
1965
|
+
"(read in the workspace's time zone), newest first. Reads need users.read:* " +
|
|
1964
1966
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1965
1967
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
1966
1968
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
@@ -2082,6 +2084,30 @@ const HOOKS = [
|
|
|
2082
2084
|
requiredContextSlice: ["directory.users"],
|
|
2083
2085
|
scopes: ["users.activity:*"],
|
|
2084
2086
|
},
|
|
2087
|
+
// sc-11066 — who was seen in a window, behind users.activity.
|
|
2088
|
+
{
|
|
2089
|
+
name: "useActiveUsers",
|
|
2090
|
+
signature: "useActiveUsers({ within, group_id, limit, offset } | null)",
|
|
2091
|
+
optionalArgs: ["within", "group_id", "limit", "offset"],
|
|
2092
|
+
description:
|
|
2093
|
+
"One page of users seen within `within`: \"online\" (default; the app " +
|
|
2094
|
+
"open in the last two minutes), \"day\" (since midnight in the " +
|
|
2095
|
+
"workspace's time zone), \"week\" or \"month\" (the last 7 / 30 days), " +
|
|
2096
|
+
"most recently seen first: `users` = [{ id, name, last_active_at, " +
|
|
2097
|
+
"last_platform }], `total` for paging. Use it to show WHO is behind an " +
|
|
2098
|
+
"online or active figure. Same gate and errors as useUserActivity; null " +
|
|
2099
|
+
"reads nothing.",
|
|
2100
|
+
returnShape: {
|
|
2101
|
+
users: "Array<{ id, name, last_active_at, last_platform }>",
|
|
2102
|
+
total: "number | null",
|
|
2103
|
+
loading: "boolean",
|
|
2104
|
+
fetching: "boolean",
|
|
2105
|
+
error: "DirectoryError | null",
|
|
2106
|
+
refetch: "() => Promise<void>",
|
|
2107
|
+
},
|
|
2108
|
+
requiredContextSlice: ["directory.users"],
|
|
2109
|
+
scopes: ["users.activity:*"],
|
|
2110
|
+
},
|
|
2085
2111
|
{
|
|
2086
2112
|
name: "useInactiveUsers",
|
|
2087
2113
|
signature: "useInactiveUsers({ inactive_days, group_id, limit, offset } | null)",
|
|
@@ -2091,7 +2117,8 @@ const HOOKS = [
|
|
|
2091
2117
|
"never-seen first, then least recently seen: `users` = [{ id, name, " +
|
|
2092
2118
|
"last_active_at, last_reminded_at }] (last_active_at null = never seen " +
|
|
2093
2119
|
"since recording began), `total` for paging. `remind(id, { title, body })` " +
|
|
2094
|
-
"notifies one of them, records when (last_reminded_at) and re-reads
|
|
2120
|
+
"notifies one of them, records when (last_reminded_at) and re-reads; " +
|
|
2121
|
+
"pass `{ refetch: false }` third to skip the re-read in a batch. " +
|
|
2095
2122
|
"Same gate and errors as useUserActivity; null reads nothing.",
|
|
2096
2123
|
returnShape: {
|
|
2097
2124
|
users: "Array<{ id, name, last_active_at, last_reminded_at }>",
|
|
@@ -2100,7 +2127,7 @@ const HOOKS = [
|
|
|
2100
2127
|
fetching: "boolean",
|
|
2101
2128
|
error: "DirectoryError | null",
|
|
2102
2129
|
refetch: "() => Promise<void>",
|
|
2103
|
-
remind: "(id, { title, body }) => Promise<void>",
|
|
2130
|
+
remind: "(id, { title, body }, { refetch? }) => Promise<void>",
|
|
2104
2131
|
},
|
|
2105
2132
|
requiredContextSlice: ["directory.users"],
|
|
2106
2133
|
scopes: ["users.activity:*"],
|
|
@@ -2920,6 +2947,13 @@ const PRIMITIVES = [
|
|
|
2920
2947
|
rnComponent: null,
|
|
2921
2948
|
docsUrl: null,
|
|
2922
2949
|
},
|
|
2950
|
+
{
|
|
2951
|
+
name: "ConfirmDialog",
|
|
2952
|
+
description:
|
|
2953
|
+
'Standard confirmation prompt, a preset of `<Modal>`. `<ConfirmDialog visible={!!pending} tone="destructive" title="Delete this order?" message="This cannot be undone." confirmLabel={t("delete", "Delete")} onCancel={() => setPending(null)} onConfirm={async () => { await remove(pending.id); setPending(null); }} />`. THE way to ask "are you sure?" before a delete or any other action that cannot be undone, so every confirmation in the app has the same wording slots, button order and colours. Never hand-build one from a `<Modal>` or an `<Overlay>` with your own two buttons, never arm a button to be pressed twice, and never use `Alert.alert` or `window.confirm`. Props: `visible` (boolean), `title` (string, the question), `message` (what confirming does; text or a node), `confirmLabel` and `cancelLabel` (default the app\'s translated "Confirm" and "Cancel" — name the action: "Delete", "Remove", "Send"), `tone` ("default" | "destructive"; destructive draws the confirm button from the theme\'s Destructive colours and opens with focus on Cancel), `busy` (boolean; disables both buttons, shows a `<Spinner>` and keeps the dialog open), `onConfirm` (return the promise of the action and the dialog stays busy until it settles, with a second press ignored; a rejection is NOT caught, so catch a failure yourself and report it), `onCancel` (fired by the Cancel button, the close button, the backdrop press, the Escape key on web and the Android back button — cancelling is always the safe default), `closeAccessibilityLabel` and `testID`. The dialog never closes itself: set `visible` false from `onCancel` and after the action. Its panel follows the theme (Design > Components > Dialogs) and its buttons the Buttons and Destructive settings — do not re-style it.',
|
|
2954
|
+
rnComponent: null,
|
|
2955
|
+
docsUrl: null,
|
|
2956
|
+
},
|
|
2923
2957
|
// sc-8104 — the in-app emoji picker. Typed emoji always worked; what this
|
|
2924
2958
|
// replaces is being forced out to the OS keyboard to find one.
|
|
2925
2959
|
{
|
|
@@ -5624,7 +5658,16 @@ const CONTRACT = deepFreeze({
|
|
|
5624
5658
|
// switch, so an existing `<Switch>` with no colour props is repainted:
|
|
5625
5659
|
// its tracks follow the theme on both hosts and its "on" thumb is white
|
|
5626
5660
|
// on the web. A caller's `trackColor` / `thumbColor` still win.
|
|
5627
|
-
|
|
5661
|
+
// 1.172.0: additive (sc-10925) — the `ConfirmDialog` primitive: the standard
|
|
5662
|
+
// confirmation, a preset of `Modal` and `Button` (title, message, confirm
|
|
5663
|
+
// and cancel labels, `tone` "default" | "destructive", `busy`). No new
|
|
5664
|
+
// theme scope: the panel reads `modal`, the actions `button` and
|
|
5665
|
+
// `destructive`. `Button` forwards a ref to its pressable.
|
|
5666
|
+
// 1.173.0: additive (sc-11066) — `useActiveUsers(query)` lists who was
|
|
5667
|
+
// seen online / today / in 7 or 30 days through the new
|
|
5668
|
+
// `ctx.directory.users.active`; `useUsers` takes `joined_since` and
|
|
5669
|
+
// `sort: "newest"` for an admin's "who joined" list.
|
|
5670
|
+
version: "1.173.0",
|
|
5628
5671
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5629
5672
|
hooks: HOOKS,
|
|
5630
5673
|
primitives: PRIMITIVES,
|