@cosmoops/connect-sdk 0.26.0 → 0.27.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/CHANGELOG.md +72 -0
- package/README.md +21 -1
- package/dist/cjs/client/components/app-shell.js +10 -1
- package/dist/cjs/client/components/sidebar.js +17 -1
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +15 -0
- package/dist/cjs/server/resources/scheduling.js +23 -0
- package/dist/esm/client/components/app-shell.js +10 -1
- package/dist/esm/client/components/sidebar.js +17 -1
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +15 -0
- package/dist/esm/server/resources/scheduling.js +23 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/scheduling.d.ts +19 -0
- package/dist/types/types.d.ts +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,78 @@
|
|
|
3
3
|
`@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
|
|
4
4
|
versions may carry breaking changes** and patch versions never do.
|
|
5
5
|
|
|
6
|
+
## 0.27.0
|
|
7
|
+
|
|
8
|
+
A minor rather than a patch because `Booking` gains a required field, so a
|
|
9
|
+
hand-written object satisfying the old interface no longer type-checks. Nothing
|
|
10
|
+
you *read* from the API breaks.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`connect.scheduling.getBooking()`** - one appointment, by the id you
|
|
15
|
+
stored.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const booking = await connect.scheduling.getBooking({
|
|
19
|
+
workspaceId,
|
|
20
|
+
bookingId, // the id you kept on your own record
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This is the read to make when a record of yours - a meeting on a deal, an
|
|
25
|
+
appointment on a case - holds a booking id. **`listBookings` cannot serve
|
|
26
|
+
it.** That is a window over `startAt`, and the `startAt` you cached is
|
|
27
|
+
exactly what is wrong once somebody has moved the appointment on the
|
|
28
|
+
workspace's own calendar, so the window you build from it comes back without
|
|
29
|
+
the row - in the one case worth re-reading for.
|
|
30
|
+
|
|
31
|
+
A retired booking still resolves, deliberately. `status: "rescheduled"`
|
|
32
|
+
answers with `rescheduledToId`, so an id stored months ago leads to the
|
|
33
|
+
appointment that exists today; follow the pointer. `cancelled` answers with
|
|
34
|
+
its reason. `not_found` means this workspace never had that booking, which is
|
|
35
|
+
a different fact from "it moved".
|
|
36
|
+
|
|
37
|
+
`GET /scheduling/booking?bookingId=` is the matching browser bridge route -
|
|
38
|
+
singular, and the id in the query string, because the bridge's route table is
|
|
39
|
+
matched by exact string. Same reason `reschedule` and `cancel` take theirs in
|
|
40
|
+
the body.
|
|
41
|
+
|
|
42
|
+
- **`Booking.hostMemberIds`** - the workspace members committed to an
|
|
43
|
+
appointment, so an integration can render who it belongs to.
|
|
44
|
+
|
|
45
|
+
These are the `WorkspaceMember.id` values `workspaces.listMembers` returns,
|
|
46
|
+
so an owner column is a lookup against a roster you already hold rather than
|
|
47
|
+
another round trip. They were withheld as "Connect internals", which
|
|
48
|
+
`listBookings` had already contradicted by *filtering* on `hostMemberId` - a
|
|
49
|
+
partner could narrow a diary by a value it was never allowed to read back.
|
|
50
|
+
|
|
51
|
+
Resolved when the slot was held and frozen there: a round-robin event type
|
|
52
|
+
picks one host, a collective one commits the whole group, and re-pointing the
|
|
53
|
+
event type afterwards does not move an appointment already made.
|
|
54
|
+
|
|
55
|
+
**This is the breaking half.** The field is required, so code constructing a
|
|
56
|
+
`Booking` literal - a test fixture, a mock, a storybook arg - needs
|
|
57
|
+
`hostMemberIds: []` added. Code that only reads bookings from the API is
|
|
58
|
+
unaffected.
|
|
59
|
+
|
|
60
|
+
## 0.26.1
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **`<ConnectAppShell>` renders a skip link.** The rail is the shell's first
|
|
65
|
+
content, so a keyboard or switch user tabbed every navigation row, the app
|
|
66
|
+
launcher and the account menu again on **every** route change before reaching
|
|
67
|
+
the page they came for - WCAG 2.4.1, and the one bypass a single-rail layout
|
|
68
|
+
cannot do without, because there is no second landmark to jump between. The
|
|
69
|
+
link is the first focusable element in the document, visually hidden until it
|
|
70
|
+
takes focus, and `<main>` now carries `id="connect-main-content"` and
|
|
71
|
+
`tabIndex={-1}` so focus genuinely lands there. Without that `tabIndex` a
|
|
72
|
+
browser scrolls to the anchor and leaves focus where it was, so the next Tab
|
|
73
|
+
returns to the top of the rail and the link appears to do nothing.
|
|
74
|
+
|
|
75
|
+
Nothing to adopt: it is on for every platform. The only visible change is a
|
|
76
|
+
"Skip to content" button that appears at the top-left on the first Tab.
|
|
77
|
+
|
|
6
78
|
## 0.26.0
|
|
7
79
|
|
|
8
80
|
A minor rather than a patch because two of these change how existing screens
|
package/README.md
CHANGED
|
@@ -345,7 +345,7 @@ try {
|
|
|
345
345
|
| `contacts` | `lookupByPhone` · `get` |
|
|
346
346
|
| `channels` | `list` · `get` · `claim` · `setRouting` · `clearRouting` |
|
|
347
347
|
| `calls` | `list` · `get` |
|
|
348
|
-
| `scheduling` | `listEventTypes` · `findSlots` · `listBookings` · `createBooking` · `rescheduleBooking` · `cancelBooking` |
|
|
348
|
+
| `scheduling` | `listEventTypes` · `findSlots` · `listBookings` · `getBooking` · `createBooking` · `rescheduleBooking` · `cancelBooking` |
|
|
349
349
|
| `ai` | `chat` · `extract` |
|
|
350
350
|
| `otp` | `send` · `verify` |
|
|
351
351
|
| `logs` | `write` |
|
|
@@ -404,6 +404,26 @@ try {
|
|
|
404
404
|
`timezone` is the **business's** zone, not your user's - render the wall time in
|
|
405
405
|
it, or you will offer an hour they are not open.
|
|
406
406
|
|
|
407
|
+
Keeping the booking id on a record of your own - a meeting on a deal, an
|
|
408
|
+
appointment on a case - is how you get back to it later:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
const booking = await client.scheduling.getBooking({ workspaceId, bookingId });
|
|
412
|
+
|
|
413
|
+
// Someone moved it on the workspace's own calendar. Follow the pointer.
|
|
414
|
+
if (booking.status === "rescheduled") {
|
|
415
|
+
const current = await client.scheduling.getBooking({
|
|
416
|
+
workspaceId,
|
|
417
|
+
bookingId: booking.rescheduledToId!,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Use `getBooking`, not `listBookings`, for that. The list is a window over
|
|
423
|
+
`startAt`, and the `startAt` you cached is exactly what is wrong once the
|
|
424
|
+
appointment has moved - so the window you build from it comes back without the
|
|
425
|
+
row, in the one case worth re-reading for.
|
|
426
|
+
|
|
407
427
|
### Attributing a log to a user
|
|
408
428
|
|
|
409
429
|
```ts
|
|
@@ -23,6 +23,15 @@ const use_active_path_js_1 = require("../use-active-path.js");
|
|
|
23
23
|
const cn_js_1 = require("../../cn.js");
|
|
24
24
|
const COLLAPSED_KEY = "connect_sidenav_collapsed";
|
|
25
25
|
const COLLAPSED_EVENT = "connect:sidenav-collapsed";
|
|
26
|
+
/**
|
|
27
|
+
* The `<main>` element's id, and the skip link's target.
|
|
28
|
+
*
|
|
29
|
+
* Module-local rather than a prop: both ends of the pair live in this file, so
|
|
30
|
+
* there is nothing for a caller to keep in step - and a partner supplying
|
|
31
|
+
* their own id could only get it wrong. Namespaced because it lands in a
|
|
32
|
+
* document the partner also writes.
|
|
33
|
+
*/
|
|
34
|
+
const CONTENT_ID = "connect-main-content";
|
|
26
35
|
/**
|
|
27
36
|
* The stored rail preference, or `null` when there isn't one (or when reading
|
|
28
37
|
* it is not allowed).
|
|
@@ -189,5 +198,5 @@ function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, r
|
|
|
189
198
|
}), [renderLink, activePath, closeMobile]);
|
|
190
199
|
const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
|
|
191
200
|
const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
|
|
192
|
-
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-
|
|
201
|
+
return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex h-dvh bg-background", className), children: [(0, jsx_runtime_1.jsx)("a", { href: `#${CONTENT_ID}`, className: "sr-only focus:not-sr-only focus:absolute focus:start-3 focus:top-3 focus:z-50 focus:rounded-lg focus:bg-surface focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-lg focus:outline-2 focus:outline-offset-2 focus:outline-focus", children: "Skip to content" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsxs)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [(0, jsx_runtime_1.jsx)(react_2.Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
|
|
193
202
|
}
|
|
@@ -100,7 +100,23 @@ function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
|
|
|
100
100
|
* the sidebar is `shrink-0`, so this is the only part that ever gives way.
|
|
101
101
|
*/
|
|
102
102
|
function ConnectSidebarBody({ children }) {
|
|
103
|
-
return (
|
|
103
|
+
return (
|
|
104
|
+
// `touch-pan-y touch-pinch-zoom` is what lets this scroll under a finger
|
|
105
|
+
// inside the mobile drawer. `Drawer.Dialog` carries an inline
|
|
106
|
+
// `touch-action: none` whenever it is dismissable - HeroUI sets it so its
|
|
107
|
+
// own pointer-driven drag-to-dismiss is not fighting the browser's pan -
|
|
108
|
+
// and the declaration meant to hand the scroll back to the scrollable
|
|
109
|
+
// region is missing from the shipped stylesheet: `@heroui/styles`'
|
|
110
|
+
// `drawer.css` carries the comment ("touch-action: pan-y allows vertical
|
|
111
|
+
// scrolling while the dialog captures drag gestures") on a `.drawer__body`
|
|
112
|
+
// rule that never sets the property. A nav longer than the panel was
|
|
113
|
+
// therefore frozen on a phone while scrolling normally under a wheel.
|
|
114
|
+
//
|
|
115
|
+
// `pinch-zoom` alongside `pan-y` rather than `pan-y` on its own: taking
|
|
116
|
+
// zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
|
|
117
|
+
// here needs it suppressed. On the desktop rail, where no ancestor
|
|
118
|
+
// restricts anything, the pair is equivalent to the default.
|
|
119
|
+
(0, jsx_runtime_1.jsx)("div", { className: "flex min-h-0 flex-1 touch-pan-y touch-pinch-zoom flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-2", children: children }));
|
|
104
120
|
}
|
|
105
121
|
/**
|
|
106
122
|
* A pinned region at the foot of the rail - the secondary nav, the launcher,
|
|
@@ -511,6 +511,21 @@ function createConnectHandler(options) {
|
|
|
511
511
|
asUser,
|
|
512
512
|
}));
|
|
513
513
|
},
|
|
514
|
+
/*
|
|
515
|
+
* Singular, and the id is a query param rather than a path segment - the
|
|
516
|
+
* table above is matched by exact string, which is the same reason
|
|
517
|
+
* reschedule and cancel take theirs in the body. Keying it as
|
|
518
|
+
* `/scheduling/bookings/{id}` would simply never match.
|
|
519
|
+
*/
|
|
520
|
+
"GET /scheduling/booking": async (request) => {
|
|
521
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
522
|
+
const asUser = await requireToken(request);
|
|
523
|
+
const bookingId = new URL(request.url).searchParams.get("bookingId");
|
|
524
|
+
if (!bookingId) {
|
|
525
|
+
return json({ error: { code: "validation_error", message: "bookingId is required" } }, 400);
|
|
526
|
+
}
|
|
527
|
+
return json(await client.scheduling.getBooking({ workspaceId, bookingId, asUser }));
|
|
528
|
+
},
|
|
514
529
|
"GET /scheduling/bookings": async (request) => {
|
|
515
530
|
const { workspaceId } = await requireWorkspace(request);
|
|
516
531
|
const asUser = await requireToken(request);
|
|
@@ -61,6 +61,29 @@ class SchedulingResource {
|
|
|
61
61
|
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* One appointment, by the id you stored.
|
|
66
|
+
*
|
|
67
|
+
* **This is the read to make when you kept a booking id on a record of your
|
|
68
|
+
* own** - a meeting on a deal, an appointment on a case. `listBookings`
|
|
69
|
+
* cannot serve it: that is a window over `startAt`, and the `startAt` you
|
|
70
|
+
* cached is exactly what is wrong once someone has moved the appointment on
|
|
71
|
+
* the workspace's own calendar, so the window you build from it comes back
|
|
72
|
+
* without the row.
|
|
73
|
+
*
|
|
74
|
+
* A retired booking still resolves. `status: "rescheduled"` answers with
|
|
75
|
+
* `rescheduledToId`, so an id stored months ago leads to the appointment
|
|
76
|
+
* that exists today; `cancelled` answers with its reason. `not_found` means
|
|
77
|
+
* this workspace never had that booking.
|
|
78
|
+
*/
|
|
79
|
+
getBooking(input) {
|
|
80
|
+
return (0, http_js_1.request)(this.transport, {
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: `/scheduling/bookings/${encodeURIComponent(input.bookingId)}`,
|
|
83
|
+
query: { workspaceId: input.workspaceId },
|
|
84
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
64
87
|
/** One page of the workspace's diary. */
|
|
65
88
|
listBookings(input) {
|
|
66
89
|
return (0, http_js_1.request)(this.transport, {
|
|
@@ -20,6 +20,15 @@ import { useActivePath } from "../use-active-path.js";
|
|
|
20
20
|
import { cn } from "../../cn.js";
|
|
21
21
|
const COLLAPSED_KEY = "connect_sidenav_collapsed";
|
|
22
22
|
const COLLAPSED_EVENT = "connect:sidenav-collapsed";
|
|
23
|
+
/**
|
|
24
|
+
* The `<main>` element's id, and the skip link's target.
|
|
25
|
+
*
|
|
26
|
+
* Module-local rather than a prop: both ends of the pair live in this file, so
|
|
27
|
+
* there is nothing for a caller to keep in step - and a partner supplying
|
|
28
|
+
* their own id could only get it wrong. Namespaced because it lands in a
|
|
29
|
+
* document the partner also writes.
|
|
30
|
+
*/
|
|
31
|
+
const CONTENT_ID = "connect-main-content";
|
|
23
32
|
/**
|
|
24
33
|
* The stored rail preference, or `null` when there isn't one (or when reading
|
|
25
34
|
* it is not allowed).
|
|
@@ -186,5 +195,5 @@ export function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, active
|
|
|
186
195
|
}), [renderLink, activePath, closeMobile]);
|
|
187
196
|
const accountRow = signedOut ? (showAuthButtons && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(ConnectLoginButton, { size: "sm", fullWidth: true }), _jsx(ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : (_jsx(ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
|
|
188
197
|
const railContent = (_jsxs(_Fragment, { children: [_jsx(ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && (_jsx("div", { className: "shrink-0 px-3 pb-1", children: _jsx(ConnectWorkspaceSwitcher, {}) })), _jsx(ConnectSidebarBody, { children: _jsx(ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && (_jsxs(ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && _jsx(ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && _jsx(ConnectAppsGrid, { variant: "row" })] })), accountRow && _jsx(ConnectSidebarFooter, { children: accountRow })] }));
|
|
189
|
-
return (_jsx(MotionProvider, { children: _jsxs("div", { className: cn("flex h-
|
|
198
|
+
return (_jsx(MotionProvider, { children: _jsxs("div", { className: cn("flex h-dvh bg-background", className), children: [_jsx("a", { href: `#${CONTENT_ID}`, className: "sr-only focus:not-sr-only focus:absolute focus:start-3 focus:top-3 focus:z-50 focus:rounded-lg focus:bg-surface focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:text-foreground focus:shadow-lg focus:outline-2 focus:outline-offset-2 focus:outline-focus", children: "Skip to content" }), _jsx(ConnectShellProvider, { value: railShell, children: _jsx(ConnectSidebar, { children: railContent }) }), _jsx(Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: _jsx(Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: _jsxs(Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]", children: [_jsx(Drawer.CloseTrigger, { className: "end-3 top-[calc(--spacing(3)+env(safe-area-inset-top))]" }), _jsx(ConnectShellProvider, { value: drawerShell, children: _jsx("div", { className: "flex h-full flex-col [&>:first-child]:pe-10", children: railContent }) })] }) }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [_jsx(Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: _jsx(Menu, { className: "size-5" }) }), _jsx(ConnectLogo, { name: name, icon: icon })] }), _jsx("main", { id: CONTENT_ID, tabIndex: -1, className: "flex-1 overflow-y-auto outline-none", children: _jsxs("div", { className: contentClassName ?? "w-full px-4 py-5 md:px-6 md:py-6", children: [gated ? _jsx(ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && _jsx(ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
|
|
190
199
|
}
|
|
@@ -93,7 +93,23 @@ export function ConnectSidebarHeader({ name, icon, homeHref = "/" }) {
|
|
|
93
93
|
* the sidebar is `shrink-0`, so this is the only part that ever gives way.
|
|
94
94
|
*/
|
|
95
95
|
export function ConnectSidebarBody({ children }) {
|
|
96
|
-
return (
|
|
96
|
+
return (
|
|
97
|
+
// `touch-pan-y touch-pinch-zoom` is what lets this scroll under a finger
|
|
98
|
+
// inside the mobile drawer. `Drawer.Dialog` carries an inline
|
|
99
|
+
// `touch-action: none` whenever it is dismissable - HeroUI sets it so its
|
|
100
|
+
// own pointer-driven drag-to-dismiss is not fighting the browser's pan -
|
|
101
|
+
// and the declaration meant to hand the scroll back to the scrollable
|
|
102
|
+
// region is missing from the shipped stylesheet: `@heroui/styles`'
|
|
103
|
+
// `drawer.css` carries the comment ("touch-action: pan-y allows vertical
|
|
104
|
+
// scrolling while the dialog captures drag gestures") on a `.drawer__body`
|
|
105
|
+
// rule that never sets the property. A nav longer than the panel was
|
|
106
|
+
// therefore frozen on a phone while scrolling normally under a wheel.
|
|
107
|
+
//
|
|
108
|
+
// `pinch-zoom` alongside `pan-y` rather than `pan-y` on its own: taking
|
|
109
|
+
// zoom away over a region of text is a WCAG 1.4.4 failure, and nothing
|
|
110
|
+
// here needs it suppressed. On the desktop rail, where no ancestor
|
|
111
|
+
// restricts anything, the pair is equivalent to the default.
|
|
112
|
+
_jsx("div", { className: "flex min-h-0 flex-1 touch-pan-y touch-pinch-zoom flex-col gap-1 overflow-y-auto overflow-x-hidden px-3 py-2", children: children }));
|
|
97
113
|
}
|
|
98
114
|
/**
|
|
99
115
|
* A pinned region at the foot of the rail - the secondary nav, the launcher,
|
|
@@ -508,6 +508,21 @@ export function createConnectHandler(options) {
|
|
|
508
508
|
asUser,
|
|
509
509
|
}));
|
|
510
510
|
},
|
|
511
|
+
/*
|
|
512
|
+
* Singular, and the id is a query param rather than a path segment - the
|
|
513
|
+
* table above is matched by exact string, which is the same reason
|
|
514
|
+
* reschedule and cancel take theirs in the body. Keying it as
|
|
515
|
+
* `/scheduling/bookings/{id}` would simply never match.
|
|
516
|
+
*/
|
|
517
|
+
"GET /scheduling/booking": async (request) => {
|
|
518
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
519
|
+
const asUser = await requireToken(request);
|
|
520
|
+
const bookingId = new URL(request.url).searchParams.get("bookingId");
|
|
521
|
+
if (!bookingId) {
|
|
522
|
+
return json({ error: { code: "validation_error", message: "bookingId is required" } }, 400);
|
|
523
|
+
}
|
|
524
|
+
return json(await client.scheduling.getBooking({ workspaceId, bookingId, asUser }));
|
|
525
|
+
},
|
|
511
526
|
"GET /scheduling/bookings": async (request) => {
|
|
512
527
|
const { workspaceId } = await requireWorkspace(request);
|
|
513
528
|
const asUser = await requireToken(request);
|
|
@@ -58,6 +58,29 @@ export class SchedulingResource {
|
|
|
58
58
|
headers: actingAs(input.asUser),
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* One appointment, by the id you stored.
|
|
63
|
+
*
|
|
64
|
+
* **This is the read to make when you kept a booking id on a record of your
|
|
65
|
+
* own** - a meeting on a deal, an appointment on a case. `listBookings`
|
|
66
|
+
* cannot serve it: that is a window over `startAt`, and the `startAt` you
|
|
67
|
+
* cached is exactly what is wrong once someone has moved the appointment on
|
|
68
|
+
* the workspace's own calendar, so the window you build from it comes back
|
|
69
|
+
* without the row.
|
|
70
|
+
*
|
|
71
|
+
* A retired booking still resolves. `status: "rescheduled"` answers with
|
|
72
|
+
* `rescheduledToId`, so an id stored months ago leads to the appointment
|
|
73
|
+
* that exists today; `cancelled` answers with its reason. `not_found` means
|
|
74
|
+
* this workspace never had that booking.
|
|
75
|
+
*/
|
|
76
|
+
getBooking(input) {
|
|
77
|
+
return request(this.transport, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
path: `/scheduling/bookings/${encodeURIComponent(input.bookingId)}`,
|
|
80
|
+
query: { workspaceId: input.workspaceId },
|
|
81
|
+
headers: actingAs(input.asUser),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
61
84
|
/** One page of the workspace's diary. */
|
|
62
85
|
listBookings(input) {
|
|
63
86
|
return request(this.transport, {
|
|
@@ -97,6 +97,25 @@ export declare class SchedulingResource {
|
|
|
97
97
|
* days rather than an error.
|
|
98
98
|
*/
|
|
99
99
|
findSlots(input: FindSlotsInput): Promise<BookingSlots>;
|
|
100
|
+
/**
|
|
101
|
+
* One appointment, by the id you stored.
|
|
102
|
+
*
|
|
103
|
+
* **This is the read to make when you kept a booking id on a record of your
|
|
104
|
+
* own** - a meeting on a deal, an appointment on a case. `listBookings`
|
|
105
|
+
* cannot serve it: that is a window over `startAt`, and the `startAt` you
|
|
106
|
+
* cached is exactly what is wrong once someone has moved the appointment on
|
|
107
|
+
* the workspace's own calendar, so the window you build from it comes back
|
|
108
|
+
* without the row.
|
|
109
|
+
*
|
|
110
|
+
* A retired booking still resolves. `status: "rescheduled"` answers with
|
|
111
|
+
* `rescheduledToId`, so an id stored months ago leads to the appointment
|
|
112
|
+
* that exists today; `cancelled` answers with its reason. `not_found` means
|
|
113
|
+
* this workspace never had that booking.
|
|
114
|
+
*/
|
|
115
|
+
getBooking(input: ActingAsOption & {
|
|
116
|
+
workspaceId: string;
|
|
117
|
+
bookingId: string;
|
|
118
|
+
}): Promise<Booking>;
|
|
100
119
|
/** One page of the workspace's diary. */
|
|
101
120
|
listBookings(input: ListBookingsInput): Promise<ListBookingsPage>;
|
|
102
121
|
/**
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1283,6 +1283,18 @@ export type BookingSource = "agent-call" | "portal" | "api";
|
|
|
1283
1283
|
export interface Booking {
|
|
1284
1284
|
id: string;
|
|
1285
1285
|
eventTypeId: string;
|
|
1286
|
+
/**
|
|
1287
|
+
* The workspace members committed to this appointment.
|
|
1288
|
+
*
|
|
1289
|
+
* These are `WorkspaceMember.id` values - the same ids `workspaces.listMembers`
|
|
1290
|
+
* returns and `listBookings`' `hostMemberId` filter takes, so an owner column
|
|
1291
|
+
* is a lookup against a roster you already have rather than another round trip.
|
|
1292
|
+
*
|
|
1293
|
+
* Resolved when the slot was held and frozen there: a round-robin event type
|
|
1294
|
+
* picks one host, a collective one commits the whole group, and re-pointing
|
|
1295
|
+
* the event type afterwards does not move an appointment already made.
|
|
1296
|
+
*/
|
|
1297
|
+
hostMemberIds: string[];
|
|
1286
1298
|
status: BookingStatus;
|
|
1287
1299
|
/** ISO-8601 instants. */
|
|
1288
1300
|
startAt: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmoops/connect-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "commonjs",
|