@cosmoops/connect-sdk 0.36.0 → 0.38.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 +35 -2
- package/dist/cjs/client/components/app-shell.js +1 -1
- package/dist/cjs/client/components/availability-form.js +75 -0
- package/dist/cjs/client/components/event-type-form.js +82 -0
- package/dist/cjs/client/components/plan-pricing.js +9 -4
- package/dist/cjs/client/components/plans.js +9 -1
- package/dist/cjs/client/components/user-menu.js +3 -24
- package/dist/cjs/client/components/workspace-switcher.js +23 -13
- package/dist/cjs/client/index.js +21 -3
- package/dist/cjs/client/portal-links.js +38 -0
- package/dist/cjs/client/use-scheduling.js +67 -8
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +78 -0
- package/dist/cjs/server/http.js +13 -1
- package/dist/cjs/server/resources/ai.js +6 -0
- package/dist/cjs/server/resources/scheduling.js +62 -0
- package/dist/cjs/server/resources/visitor.js +6 -0
- package/dist/esm/client/components/app-shell.js +1 -1
- package/dist/esm/client/components/availability-form.js +72 -0
- package/dist/esm/client/components/event-type-form.js +79 -0
- package/dist/esm/client/components/plan-pricing.js +9 -4
- package/dist/esm/client/components/plans.js +9 -1
- package/dist/esm/client/components/user-menu.js +3 -24
- package/dist/esm/client/components/workspace-switcher.js +26 -16
- package/dist/esm/client/index.js +12 -1
- package/dist/esm/client/portal-links.js +34 -0
- package/dist/esm/client/use-scheduling.js +64 -8
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +78 -0
- package/dist/esm/server/http.js +12 -1
- package/dist/esm/server/resources/ai.js +7 -1
- package/dist/esm/server/resources/scheduling.js +62 -0
- package/dist/esm/server/resources/visitor.js +7 -1
- package/dist/types/client/components/app-shell.d.ts +7 -1
- package/dist/types/client/components/availability-form.d.ts +22 -0
- package/dist/types/client/components/event-type-form.d.ts +22 -0
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/workspace-switcher.d.ts +25 -6
- package/dist/types/client/index.d.ts +6 -1
- package/dist/types/client/portal-links.d.ts +22 -0
- package/dist/types/client/use-scheduling.d.ts +49 -1
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/http.d.ts +35 -0
- package/dist/types/server/resources/scheduling.d.ts +67 -1
- package/dist/types/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { Button, Dropdown, ListBox } from "@heroui/react";
|
|
5
|
-
import { ChevronsUpDown } from "lucide-react";
|
|
4
|
+
import { Button, Dropdown, ListBox, Separator } from "@heroui/react";
|
|
5
|
+
import { ChevronsUpDown, Plus } from "lucide-react";
|
|
6
6
|
import { useConnect, useConnectUser, useConnectWorkspaces } from "../provider.js";
|
|
7
7
|
import { m } from "./motion.js";
|
|
8
8
|
import { SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
@@ -11,6 +11,9 @@ function workspaceInitials(name) {
|
|
|
11
11
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
12
12
|
return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
|
|
13
13
|
}
|
|
14
|
+
function openExternal(url) {
|
|
15
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
16
|
+
}
|
|
14
17
|
/**
|
|
15
18
|
* Switches which workspace this application is looking at.
|
|
16
19
|
*
|
|
@@ -19,11 +22,15 @@ function workspaceInitials(name) {
|
|
|
19
22
|
* it reads as a heading for the nav rather than as one more destination.
|
|
20
23
|
*
|
|
21
24
|
* It decides for itself whether to appear: an account with one workspace is
|
|
22
|
-
* shown nothing, because a switcher offering one option teaches
|
|
23
|
-
* nothing and costs a row of chrome on every screen. That is the
|
|
24
|
-
* Connect's own switcher, which always renders - Connect is where
|
|
25
|
-
* is *created*, so hiding it at one workspace would guarantee
|
|
26
|
-
* reached two.
|
|
25
|
+
* shown nothing by default, because a switcher offering one option teaches
|
|
26
|
+
* the user nothing and costs a row of chrome on every screen. That is the
|
|
27
|
+
* opposite of Connect's own switcher, which always renders - Connect is where
|
|
28
|
+
* a workspace is *created*, so hiding it at one workspace would guarantee
|
|
29
|
+
* nobody ever reached two. `alwaysShow` is the escape hatch for a partner
|
|
30
|
+
* application that wants that same discoverability without becoming Connect:
|
|
31
|
+
* it keeps the dropdown open below two workspaces and adds a row that sends
|
|
32
|
+
* the member to Connect to actually create one, since there is nothing here
|
|
33
|
+
* that can.
|
|
27
34
|
*
|
|
28
35
|
* **Authorization is the data, not a prop.** The list comes from the caller's
|
|
29
36
|
* own session (`GET /workspaces` takes no user id), so a workspace the user
|
|
@@ -42,19 +49,22 @@ function workspaceInitials(name) {
|
|
|
42
49
|
* component - so the only thing to do afterwards is reload, which is what
|
|
43
50
|
* `onSwitched` defaults to.
|
|
44
51
|
*/
|
|
45
|
-
export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
|
|
46
|
-
const { mutateResource } = useConnect();
|
|
52
|
+
export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysShow = false, }) {
|
|
53
|
+
const { mutateResource, appUrl } = useConnect();
|
|
47
54
|
const { collapsed } = useConnectShell();
|
|
48
55
|
const user = useConnectUser();
|
|
49
56
|
const { data, isLoading } = useConnectWorkspaces();
|
|
50
57
|
const [switching, setSwitching] = useState(null);
|
|
51
58
|
const [error, setError] = useState(null);
|
|
52
59
|
const workspaces = data?.workspaces ?? [];
|
|
53
|
-
// Nothing
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
60
|
+
// Nothing loaded yet. Deliberately renders `null` rather than a skeleton:
|
|
61
|
+
// this sits in persistent chrome, and a placeholder that resolves to
|
|
62
|
+
// nothing for most accounts is worse than appearing a moment late.
|
|
63
|
+
//
|
|
64
|
+
// Below one workspace, `alwaysShow` is the only thing that keeps this
|
|
65
|
+
// rendering - the default stays "nothing to switch between" for every
|
|
66
|
+
// host that hasn't opted in.
|
|
67
|
+
if (isLoading || (!alwaysShow && workspaces.length < 2))
|
|
58
68
|
return null;
|
|
59
69
|
const active = workspaces.find((workspace) => workspace.isActive);
|
|
60
70
|
const label = active?.name ?? user?.workspaceName ?? "Workspace";
|
|
@@ -84,7 +94,7 @@ export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, })
|
|
|
84
94
|
// A 32px tile rather than a 20px glyph, so the initials land on the
|
|
85
95
|
// same optical centre the avatar below them uses - see
|
|
86
96
|
// `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
|
|
87
|
-
_jsxs(Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: sidebarRowClass({ tile: true, className }), children: [_jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-xs font-semibold text-accent", children: workspaceInitials(label) }), _jsx(SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), _jsx(m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: label }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), _jsx(Dropdown.Popover, { children:
|
|
97
|
+
_jsxs(Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: sidebarRowClass({ tile: true, className }), children: [_jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-xs font-semibold text-accent", children: workspaceInitials(label) }), _jsx(SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), _jsx(m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: label }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), _jsx(Dropdown.Popover, { children: _jsxs(ListBox, { children: [workspaces.map((workspace) => (_jsxs(ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : (_jsx("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), _jsx(ListBox.ItemIndicator, {})] }, workspace.workspaceId))), alwaysShow ? (_jsxs(_Fragment, { children: [workspaces.length > 0 && _jsx(Separator, {}), _jsxs(ListBox.Item, { id: "__create_workspace", textValue: "Create workspace", onAction: () => openExternal(appUrl), children: [_jsx(Plus, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), _jsx("p", { className: "truncate text-sm", children: "Create workspace" })] })] })) : null] }) })] }), error && !collapsed ? _jsx("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
|
|
88
98
|
}
|
|
89
99
|
/** Belongs to the user, and usable here. The same two facts `switch` enforces. */
|
|
90
100
|
function selectable(workspace) {
|
package/dist/esm/client/index.js
CHANGED
|
@@ -31,6 +31,11 @@ ConnectUserMenu, ConnectManageSubscriptionButton, ConnectInvoicesButton, Connect
|
|
|
31
31
|
// Built, compiled into `dist/`, and reachable from no entry point until now -
|
|
32
32
|
// `ConnectUserMenu` was the only thing that could render it.
|
|
33
33
|
export { ConnectAccountSessions } from "./components/account-sessions.js";
|
|
34
|
+
// A link to *any* Connect portal page that offers a "Back to <your app>" way
|
|
35
|
+
// home - what `ConnectProfileButton` builds for `/profile` internally, made
|
|
36
|
+
// reusable for a host linking out to a different Connect page (e.g.
|
|
37
|
+
// `/calendar/settings`) on its own.
|
|
38
|
+
export { connectPortalUrl, useConnectPortalUrl } from "./portal-links.js";
|
|
34
39
|
// The frame the rest of these components live inside. Exported next to the
|
|
35
40
|
// pieces it composes rather than in a section of its own - an integration
|
|
36
41
|
// reaching for a sidebar wants to find the launcher and the account menu in
|
|
@@ -144,5 +149,11 @@ export { ConnectWorkspaceSwitcher } from "./components/workspace-switcher.js";
|
|
|
144
149
|
export { useConnectWorkspaces, useConnectWorkspaceMembers } from "./provider.js";
|
|
145
150
|
export { ConnectBookingCalendar } from "./components/booking-calendar.js";
|
|
146
151
|
export { ConnectBookingDialog, ConnectBookingDetail } from "./components/booking-dialogs.js";
|
|
147
|
-
|
|
152
|
+
// The write half of scheduling - a workspace's own first-time setup, not a
|
|
153
|
+
// partner integration's booking flow. `ConnectAvailabilityFormModal` and
|
|
154
|
+
// `ConnectEventTypeFormModal` give a host application a real inline setup
|
|
155
|
+
// step instead of a link-out to Connect's own Calendar settings.
|
|
156
|
+
export { ConnectAvailabilityFormModal } from "./components/availability-form.js";
|
|
157
|
+
export { ConnectEventTypeFormModal } from "./components/event-type-form.js";
|
|
158
|
+
export { useBookableEventTypes, useAvailabilitySchedules, useCreateAvailabilitySchedule, useCreateEventType, useAvailableSlots, useBookings, useBookingActions, } from "./use-scheduling.js";
|
|
148
159
|
export { layoutBookings, dayColumns, zonedInstant, } from "./components/booking-grid.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useConnect } from "./provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* Any Connect portal page, with the context it needs to offer a way back
|
|
5
|
+
* here - `connectPortalUrl(appUrl, clientId, "/profile")`,
|
|
6
|
+
* `connectPortalUrl(appUrl, clientId, "/calendar/settings")`, and so on.
|
|
7
|
+
*
|
|
8
|
+
* Connect answers any of these paths for a signed-in user on the bare URL -
|
|
9
|
+
* these two params only add the "Back to <your app>" banner
|
|
10
|
+
* (`ApplicationReturnBar` on Connect's side). It validates `return_to`
|
|
11
|
+
* against this `client_id`'s registered redirect origins and silently drops
|
|
12
|
+
* the banner if they don't match, so a wrong or stale `appUrl`/`clientId`
|
|
13
|
+
* pairing degrades to what linking there did before rather than erroring.
|
|
14
|
+
*
|
|
15
|
+
* `window.location.href` rather than a configured home URL: the point is to
|
|
16
|
+
* come back to the page they left, and only the browser knows which one that
|
|
17
|
+
* is.
|
|
18
|
+
*/
|
|
19
|
+
export function connectPortalUrl(appUrl, clientId, path) {
|
|
20
|
+
const url = new URL(path, appUrl);
|
|
21
|
+
url.searchParams.set("client_id", clientId);
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
url.searchParams.set("return_to", window.location.href);
|
|
24
|
+
}
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `connectPortalUrl`, with `appUrl`/`clientId` read from the provider instead
|
|
29
|
+
* of threaded through by every caller.
|
|
30
|
+
*/
|
|
31
|
+
export function useConnectPortalUrl(path) {
|
|
32
|
+
const { appUrl, clientId } = useConnect();
|
|
33
|
+
return connectPortalUrl(appUrl, clientId, path);
|
|
34
|
+
}
|
|
@@ -15,6 +15,48 @@ export function useBookableEventTypes() {
|
|
|
15
15
|
const { data, isLoading, error } = useConnectResource("/scheduling/event-types");
|
|
16
16
|
return { eventTypes: data?.eventTypes ?? [], isLoading, error };
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The working-hours templates an appointment type can point at.
|
|
20
|
+
*
|
|
21
|
+
* Read this before offering a "set up availability" step - a workspace that
|
|
22
|
+
* already has one does not need a second "Working hours" from your UI.
|
|
23
|
+
*/
|
|
24
|
+
export function useAvailabilitySchedules() {
|
|
25
|
+
const { data, isLoading, error } = useConnectResource("/scheduling/schedules");
|
|
26
|
+
return { schedules: data?.schedules ?? [], isLoading, error };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gives a workspace its first working hours.
|
|
30
|
+
*
|
|
31
|
+
* **Not optimistic** - same posture as `useBookingActions`: this is one of
|
|
32
|
+
* the two writes a host's first-run setup makes, and a modal that closes on
|
|
33
|
+
* a failed create is worse than one that stays open with the reason shown.
|
|
34
|
+
*/
|
|
35
|
+
export function useCreateAvailabilitySchedule() {
|
|
36
|
+
const { run, isPending, error, clearError } = useResourceAction();
|
|
37
|
+
return {
|
|
38
|
+
isPending,
|
|
39
|
+
error,
|
|
40
|
+
clearError,
|
|
41
|
+
createSchedule: (input) => run("/scheduling/schedules", input),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates the first thing a workspace can be booked for.
|
|
46
|
+
*
|
|
47
|
+
* Host is a specific member, never a team - there is no groups read in this
|
|
48
|
+
* bridge, so a round-robin team host stays a Console-only capability
|
|
49
|
+
* reachable by following "Manage in Connect".
|
|
50
|
+
*/
|
|
51
|
+
export function useCreateEventType() {
|
|
52
|
+
const { run, isPending, error, clearError } = useResourceAction();
|
|
53
|
+
return {
|
|
54
|
+
isPending,
|
|
55
|
+
error,
|
|
56
|
+
clearError,
|
|
57
|
+
createEventType: (input) => run("/scheduling/event-types", input),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
18
60
|
/**
|
|
19
61
|
* Free times for one appointment type.
|
|
20
62
|
*
|
|
@@ -68,15 +110,16 @@ export function useBookings(query) {
|
|
|
68
110
|
};
|
|
69
111
|
}
|
|
70
112
|
/**
|
|
71
|
-
*
|
|
113
|
+
* The isPending/error bookkeeping every write in this file shares, factored
|
|
114
|
+
* out once `useCreateAvailabilitySchedule` and `useCreateEventType` needed
|
|
115
|
+
* the identical shape `useBookingActions` already had.
|
|
72
116
|
*
|
|
73
|
-
* **
|
|
74
|
-
* slot is
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* the two-phase hold exists to prevent.
|
|
117
|
+
* **Never optimistic**, and that is not an oversight for any caller of this:
|
|
118
|
+
* whether a slot is free, or a schedule name collides, is a question only the
|
|
119
|
+
* server can answer. Painting the result before it comes back is exactly the
|
|
120
|
+
* lie a two-phase hold (or a modal that closes early) would tell.
|
|
78
121
|
*/
|
|
79
|
-
|
|
122
|
+
function useResourceAction() {
|
|
80
123
|
const { mutateResource } = useConnect();
|
|
81
124
|
const [isPending, setPending] = useState(false);
|
|
82
125
|
const [error, setError] = useState(null);
|
|
@@ -96,10 +139,23 @@ export function useBookingActions() {
|
|
|
96
139
|
setPending(false);
|
|
97
140
|
}
|
|
98
141
|
}, [mutateResource]);
|
|
142
|
+
return { isPending, error, clearError: () => setError(null), run };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Book, move and cancel.
|
|
146
|
+
*
|
|
147
|
+
* **None of these is optimistic**, and that is not an oversight. Whether a
|
|
148
|
+
* slot is still free is a question only the server can answer, inside the
|
|
149
|
+
* transaction that decides it - two people can be holding the same minute
|
|
150
|
+
* right now. Painting a booking as made and withdrawing it is exactly the lie
|
|
151
|
+
* the two-phase hold exists to prevent.
|
|
152
|
+
*/
|
|
153
|
+
export function useBookingActions() {
|
|
154
|
+
const { run, isPending, error, clearError } = useResourceAction();
|
|
99
155
|
return {
|
|
100
156
|
isPending,
|
|
101
157
|
error,
|
|
102
|
-
clearError
|
|
158
|
+
clearError,
|
|
103
159
|
createBooking: (input) => run("/scheduling/bookings", input),
|
|
104
160
|
rescheduleBooking: (input) => run("/scheduling/bookings/reschedule", input).then((result) => result?.booking ?? null),
|
|
105
161
|
cancelBooking: (input) => run("/scheduling/bookings/cancel", input),
|
|
@@ -850,6 +850,84 @@ export function createConnectHandler(options) {
|
|
|
850
850
|
const asUser = await requireToken(request);
|
|
851
851
|
return json({ eventTypes: await client.scheduling.listEventTypes(workspaceId, { asUser }) });
|
|
852
852
|
},
|
|
853
|
+
/*
|
|
854
|
+
* Creates the first thing a workspace can be booked for - a browser-side
|
|
855
|
+
* counterpart to `POST /scheduling/event-types` above, for a host's own
|
|
856
|
+
* inline setup UI (`ConnectEventTypeFormModal`). Host is a specific
|
|
857
|
+
* member, never a team - there is no groups read in this bridge, so a
|
|
858
|
+
* round-robin team host stays a Console-only capability.
|
|
859
|
+
*/
|
|
860
|
+
"POST /scheduling/event-types": async (request) => {
|
|
861
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
862
|
+
const asUser = await requireToken(request);
|
|
863
|
+
const body = (await request.json().catch(() => null));
|
|
864
|
+
if (typeof body?.name !== "string" ||
|
|
865
|
+
typeof body.durationMinutes !== "number" ||
|
|
866
|
+
typeof body.hostMemberId !== "string" ||
|
|
867
|
+
typeof body.scheduleId !== "string" ||
|
|
868
|
+
typeof body.location?.kind !== "string") {
|
|
869
|
+
throw new ConnectError({
|
|
870
|
+
status: 400,
|
|
871
|
+
code: "validation_error",
|
|
872
|
+
message: "name, durationMinutes, hostMemberId, scheduleId and location are required",
|
|
873
|
+
endpoint: "/scheduling/event-types",
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return json(await client.scheduling.createEventType({
|
|
877
|
+
workspaceId,
|
|
878
|
+
name: body.name,
|
|
879
|
+
description: typeof body.description === "string" ? body.description : null,
|
|
880
|
+
durationMinutes: body.durationMinutes,
|
|
881
|
+
bufferBeforeMinutes: typeof body.bufferBeforeMinutes === "number" ? body.bufferBeforeMinutes : undefined,
|
|
882
|
+
bufferAfterMinutes: typeof body.bufferAfterMinutes === "number" ? body.bufferAfterMinutes : undefined,
|
|
883
|
+
hostMemberId: body.hostMemberId,
|
|
884
|
+
scheduleId: body.scheduleId,
|
|
885
|
+
location: {
|
|
886
|
+
kind: body.location.kind,
|
|
887
|
+
value: typeof body.location.value === "string" ? body.location.value : null,
|
|
888
|
+
},
|
|
889
|
+
publicBookingEnabled: typeof body.publicBookingEnabled === "boolean" ? body.publicBookingEnabled : undefined,
|
|
890
|
+
asUser,
|
|
891
|
+
}), 201);
|
|
892
|
+
},
|
|
893
|
+
/*
|
|
894
|
+
* The working-hours templates an appointment type can point at - read
|
|
895
|
+
* before a setup UI offers to create one, so it can tell "none yet" from
|
|
896
|
+
* "already set up".
|
|
897
|
+
*/
|
|
898
|
+
"GET /scheduling/schedules": async (request) => {
|
|
899
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
900
|
+
const asUser = await requireToken(request);
|
|
901
|
+
return json({ schedules: await client.scheduling.listSchedules(workspaceId, { asUser }) });
|
|
902
|
+
},
|
|
903
|
+
/**
|
|
904
|
+
* Gives a workspace its first working hours - the browser-side
|
|
905
|
+
* counterpart to `POST /scheduling/schedules` above, for
|
|
906
|
+
* `ConnectAvailabilityFormModal`.
|
|
907
|
+
*/
|
|
908
|
+
"POST /scheduling/schedules": async (request) => {
|
|
909
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
910
|
+
const asUser = await requireToken(request);
|
|
911
|
+
const body = (await request.json().catch(() => null));
|
|
912
|
+
if (typeof body?.name !== "string" ||
|
|
913
|
+
typeof body.timezone !== "string" ||
|
|
914
|
+
typeof body.weeklyHours !== "object" ||
|
|
915
|
+
body.weeklyHours === null) {
|
|
916
|
+
throw new ConnectError({
|
|
917
|
+
status: 400,
|
|
918
|
+
code: "validation_error",
|
|
919
|
+
message: "name, timezone and weeklyHours are required",
|
|
920
|
+
endpoint: "/scheduling/schedules",
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
return json(await client.scheduling.createSchedule({
|
|
924
|
+
workspaceId,
|
|
925
|
+
name: body.name,
|
|
926
|
+
timezone: body.timezone,
|
|
927
|
+
weeklyHours: body.weeklyHours,
|
|
928
|
+
asUser,
|
|
929
|
+
}), 201);
|
|
930
|
+
},
|
|
853
931
|
"GET /scheduling/slots": async (request) => {
|
|
854
932
|
const { workspaceId } = await requireWorkspace(request);
|
|
855
933
|
const asUser = await requireToken(request);
|
package/dist/esm/server/http.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { ConnectError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* How long a *generating* endpoint may take to send its headers.
|
|
4
|
+
*
|
|
5
|
+
* One number, named once, and it is not a guess: it is what
|
|
6
|
+
* `POST /visitor/chat` and `POST /ai/chat` declare as their `maxDuration` on
|
|
7
|
+
* the far side. A client bound shorter than the route's own ceiling cannot be
|
|
8
|
+
* right - it can only abort work Connect is still allowed to be doing - and
|
|
9
|
+
* the two numbers drifting apart is exactly how a 20s client came to be
|
|
10
|
+
* fronting a 60s route.
|
|
11
|
+
*/
|
|
12
|
+
export const GENERATION_TIMEOUT_MS = 60_000;
|
|
2
13
|
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
3
14
|
/**
|
|
4
15
|
* One id, safe to interpolate into a request path.
|
|
@@ -145,7 +156,7 @@ export async function request(options, spec) {
|
|
|
145
156
|
// that has actually started to run as long as it needs. A caller that
|
|
146
157
|
// wants to bound the stream itself passes `spec.signal`.
|
|
147
158
|
const timeoutController = new AbortController();
|
|
148
|
-
const timer = setTimeout(() => timeoutController.abort(), options.timeoutMs);
|
|
159
|
+
const timer = setTimeout(() => timeoutController.abort(), spec.timeoutMs ?? options.timeoutMs);
|
|
149
160
|
const signal = spec.signal
|
|
150
161
|
? AbortSignal.any([spec.signal, timeoutController.signal])
|
|
151
162
|
: timeoutController.signal;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { actingAs } from "./acting-as.js";
|
|
2
|
-
import { request } from "../http.js";
|
|
2
|
+
import { GENERATION_TIMEOUT_MS, request } from "../http.js";
|
|
3
3
|
/**
|
|
4
4
|
* RAG chat and structured extraction against a workspace's data.
|
|
5
5
|
*
|
|
@@ -40,6 +40,12 @@ export class AiResource {
|
|
|
40
40
|
...(input.screenContext ? { screenContext: input.screenContext } : {}),
|
|
41
41
|
},
|
|
42
42
|
headers: actingAs(input.asUser),
|
|
43
|
+
/*
|
|
44
|
+
* Matches the route's own `maxDuration`. This endpoint does not send
|
|
45
|
+
* headers until the model's first token, so the default 20s bound was
|
|
46
|
+
* aborting any turn that opened with a tool call - see `RequestSpec.timeoutMs`.
|
|
47
|
+
*/
|
|
48
|
+
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
43
49
|
raw: true,
|
|
44
50
|
signal: options.signal,
|
|
45
51
|
});
|
|
@@ -31,6 +31,68 @@ export class SchedulingResource {
|
|
|
31
31
|
// `undefined` past every status check in between.
|
|
32
32
|
return result.eventTypes ?? [];
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The working-hours templates an appointment type can point at.
|
|
36
|
+
*
|
|
37
|
+
* Read this before offering to create one - a workspace that already has
|
|
38
|
+
* hours set does not need a second "Working hours" from your integration.
|
|
39
|
+
*/
|
|
40
|
+
async listSchedules(workspaceId, options = {}) {
|
|
41
|
+
const result = await request(this.transport, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
path: "/scheduling/schedules",
|
|
44
|
+
query: { workspaceId },
|
|
45
|
+
headers: actingAs(options.asUser),
|
|
46
|
+
});
|
|
47
|
+
return result.schedules ?? [];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gives a workspace its first working hours.
|
|
51
|
+
*
|
|
52
|
+
* Narrower than what the workspace's own Calendar settings can do - no date
|
|
53
|
+
* overrides, and never owned by one person (`ownerMemberId` stays `null`
|
|
54
|
+
* server-side). Anything beyond that is a Console edit, not a second write
|
|
55
|
+
* path for your integration to maintain.
|
|
56
|
+
*/
|
|
57
|
+
createSchedule(input) {
|
|
58
|
+
return request(this.transport, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/scheduling/schedules",
|
|
61
|
+
json: {
|
|
62
|
+
workspaceId: input.workspaceId,
|
|
63
|
+
name: input.name,
|
|
64
|
+
timezone: input.timezone,
|
|
65
|
+
weeklyHours: input.weeklyHours,
|
|
66
|
+
},
|
|
67
|
+
headers: actingAs(input.asUser),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The first thing a workspace can be booked for.
|
|
72
|
+
*
|
|
73
|
+
* Needs a schedule - create or pick one with `listSchedules`/`createSchedule`
|
|
74
|
+
* first. The slug for a future public booking link is derived from `name`
|
|
75
|
+
* server-side; you cannot choose one.
|
|
76
|
+
*/
|
|
77
|
+
createEventType(input) {
|
|
78
|
+
return request(this.transport, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
path: "/scheduling/event-types",
|
|
81
|
+
json: {
|
|
82
|
+
workspaceId: input.workspaceId,
|
|
83
|
+
name: input.name,
|
|
84
|
+
description: input.description ?? null,
|
|
85
|
+
durationMinutes: input.durationMinutes,
|
|
86
|
+
bufferBeforeMinutes: input.bufferBeforeMinutes,
|
|
87
|
+
bufferAfterMinutes: input.bufferAfterMinutes,
|
|
88
|
+
hostMemberId: input.hostMemberId,
|
|
89
|
+
scheduleId: input.scheduleId,
|
|
90
|
+
location: input.location,
|
|
91
|
+
publicBookingEnabled: input.publicBookingEnabled ?? false,
|
|
92
|
+
},
|
|
93
|
+
headers: actingAs(input.asUser),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
34
96
|
/**
|
|
35
97
|
* Free times for one appointment type over one window.
|
|
36
98
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { request } from "../http.js";
|
|
1
|
+
import { GENERATION_TIMEOUT_MS, request } from "../http.js";
|
|
2
2
|
/**
|
|
3
3
|
* The header a bridge forwards the visitor's own address on.
|
|
4
4
|
*
|
|
@@ -97,6 +97,12 @@ export class VisitorResource {
|
|
|
97
97
|
path: "/visitor/chat",
|
|
98
98
|
json: input,
|
|
99
99
|
headers: visitorHeaders(options),
|
|
100
|
+
/*
|
|
101
|
+
* Matches the route's own `maxDuration`. This endpoint does not send
|
|
102
|
+
* headers until the model's first token, so the default 20s bound was
|
|
103
|
+
* aborting any turn that opened with a tool call - see `RequestSpec.timeoutMs`.
|
|
104
|
+
*/
|
|
105
|
+
timeoutMs: GENERATION_TIMEOUT_MS,
|
|
100
106
|
raw: true,
|
|
101
107
|
signal: options.signal,
|
|
102
108
|
});
|
|
@@ -36,8 +36,14 @@ export interface AppShellProps {
|
|
|
36
36
|
* for an account with only one workspace - so the reason to pass `false` is
|
|
37
37
|
* not "most users have one" (already handled) but "this application renders
|
|
38
38
|
* its own".
|
|
39
|
+
*
|
|
40
|
+
* Pass `{ alwaysShow: true }` instead of `true` to keep it mounted below
|
|
41
|
+
* two workspaces too - see `ConnectWorkspaceSwitcherProps.alwaysShow` for
|
|
42
|
+
* what that changes about the dropdown.
|
|
39
43
|
*/
|
|
40
|
-
workspaceSwitcher?: false
|
|
44
|
+
workspaceSwitcher?: false | {
|
|
45
|
+
alwaysShow?: boolean;
|
|
46
|
+
};
|
|
41
47
|
/** Rendered below `children`, inside the scrolling content pane. */
|
|
42
48
|
footer?: ReactNode;
|
|
43
49
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AvailabilitySchedule } from "../../types.js";
|
|
2
|
+
export interface ConnectAvailabilityFormModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
5
|
+
/** Called once the schedule is created, before the modal closes. */
|
|
6
|
+
onCreated?: (schedule: AvailabilitySchedule) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Gives a workspace its first working hours, from inside a host application
|
|
10
|
+
* rather than a link-out to Connect.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately narrower than Connect's own Calendar settings form: one
|
|
13
|
+
* interval per day (no split shifts), no date overrides, and the timezone
|
|
14
|
+
* defaults to the browser's own rather than asking. This is a first-run
|
|
15
|
+
* setup step, not a second copy of the Console editor - "Manage in Connect"
|
|
16
|
+
* is still where the rest of what a schedule can do lives.
|
|
17
|
+
*
|
|
18
|
+
* **Not optimistic.** `useCreateAvailabilitySchedule` settles the write
|
|
19
|
+
* against the server before this closes, the same posture every write in
|
|
20
|
+
* this package takes - see `ConnectBookingDialog`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ConnectAvailabilityFormModal({ isOpen, onOpenChange, onCreated, }: Readonly<ConnectAvailabilityFormModalProps>): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BookableEventType } from "../../types.js";
|
|
2
|
+
export interface ConnectEventTypeFormModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
5
|
+
/** Called once the appointment type is created, before the modal closes. */
|
|
6
|
+
onCreated?: (eventType: BookableEventType) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates the first thing a workspace can be booked for, from inside a host
|
|
10
|
+
* application rather than a link-out to Connect.
|
|
11
|
+
*
|
|
12
|
+
* **Needs a schedule to exist first** - the picker reads `useAvailabilitySchedules`
|
|
13
|
+
* and renders "Set up availability first" with nothing to submit if none
|
|
14
|
+
* exists yet. Pair this with `ConnectAvailabilityFormModal` in a setup flow
|
|
15
|
+
* rather than assuming one.
|
|
16
|
+
*
|
|
17
|
+
* **Host is a specific member, never a team.** There is no groups read in
|
|
18
|
+
* this bridge, so a round-robin team host stays a Console-only capability -
|
|
19
|
+
* defaults to whoever is signed in, with an optional picker from the
|
|
20
|
+
* workspace's own roster.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, }: Readonly<ConnectEventTypeFormModalProps>): import("react").JSX.Element;
|
|
@@ -74,6 +74,16 @@ export interface PlanTier {
|
|
|
74
74
|
description: string;
|
|
75
75
|
monthly: Plan | null;
|
|
76
76
|
annual: Plan | null;
|
|
77
|
+
/**
|
|
78
|
+
* A plan paid once, never on a cadence - its own slot rather than falling
|
|
79
|
+
* into `annual`. A one-time plan has no renewal to discount against, so
|
|
80
|
+
* filing it as "annual" used to let a tier mixing a recurring plan with a
|
|
81
|
+
* one-time one compute a `annualSavingPercent` between a monthly charge and
|
|
82
|
+
* a lump sum, and offered the one-time purchase under the annual toggle as
|
|
83
|
+
* though it renewed. `pickTierPlan` in `plans.tsx` is the only reader that
|
|
84
|
+
* needs to know this slot exists.
|
|
85
|
+
*/
|
|
86
|
+
oneTime: Plan | null;
|
|
77
87
|
/** Whole-percent saving from paying annually, or `null` when this tier has no pair to compare. */
|
|
78
88
|
annualSavingPercent: number | null;
|
|
79
89
|
}
|
|
@@ -14,6 +14,21 @@ export interface ConnectWorkspaceSwitcherProps {
|
|
|
14
14
|
*/
|
|
15
15
|
row?: boolean;
|
|
16
16
|
className?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Render even when the signed-in account has fewer than two workspaces.
|
|
19
|
+
*
|
|
20
|
+
* Off by default, which keeps this component's documented posture for
|
|
21
|
+
* every other host: an account with one workspace sees nothing, because a
|
|
22
|
+
* switcher offering one option teaches it nothing. Pass `true` on an
|
|
23
|
+
* application that wants a single-workspace account to be able to
|
|
24
|
+
* discover - and start - creating a second one without leaving its own
|
|
25
|
+
* chrome. There is deliberately no `workspaces.create()` on the SDK:
|
|
26
|
+
* creating a workspace is Connect's job, tied to KYC/KYB and a billing
|
|
27
|
+
* profile, not something an API key should be able to trigger - so turning
|
|
28
|
+
* this on also adds a "Create workspace" row that opens Connect, rather
|
|
29
|
+
* than pretending the switcher can do it itself.
|
|
30
|
+
*/
|
|
31
|
+
alwaysShow?: boolean;
|
|
17
32
|
}
|
|
18
33
|
/**
|
|
19
34
|
* Switches which workspace this application is looking at.
|
|
@@ -23,11 +38,15 @@ export interface ConnectWorkspaceSwitcherProps {
|
|
|
23
38
|
* it reads as a heading for the nav rather than as one more destination.
|
|
24
39
|
*
|
|
25
40
|
* It decides for itself whether to appear: an account with one workspace is
|
|
26
|
-
* shown nothing, because a switcher offering one option teaches
|
|
27
|
-
* nothing and costs a row of chrome on every screen. That is the
|
|
28
|
-
* Connect's own switcher, which always renders - Connect is where
|
|
29
|
-
* is *created*, so hiding it at one workspace would guarantee
|
|
30
|
-
* reached two.
|
|
41
|
+
* shown nothing by default, because a switcher offering one option teaches
|
|
42
|
+
* the user nothing and costs a row of chrome on every screen. That is the
|
|
43
|
+
* opposite of Connect's own switcher, which always renders - Connect is where
|
|
44
|
+
* a workspace is *created*, so hiding it at one workspace would guarantee
|
|
45
|
+
* nobody ever reached two. `alwaysShow` is the escape hatch for a partner
|
|
46
|
+
* application that wants that same discoverability without becoming Connect:
|
|
47
|
+
* it keeps the dropdown open below two workspaces and adds a row that sends
|
|
48
|
+
* the member to Connect to actually create one, since there is nothing here
|
|
49
|
+
* that can.
|
|
31
50
|
*
|
|
32
51
|
* **Authorization is the data, not a prop.** The list comes from the caller's
|
|
33
52
|
* own session (`GET /workspaces` takes no user id), so a workspace the user
|
|
@@ -46,4 +65,4 @@ export interface ConnectWorkspaceSwitcherProps {
|
|
|
46
65
|
* component - so the only thing to do afterwards is reload, which is what
|
|
47
66
|
* `onSwitched` defaults to.
|
|
48
67
|
*/
|
|
49
|
-
export declare function ConnectWorkspaceSwitcher({ onSwitched, row, className, }: Readonly<ConnectWorkspaceSwitcherProps>): import("react").JSX.Element | null;
|
|
68
|
+
export declare function ConnectWorkspaceSwitcher({ onSwitched, row, className, alwaysShow, }: Readonly<ConnectWorkspaceSwitcherProps>): import("react").JSX.Element | null;
|
|
@@ -32,6 +32,7 @@ export type { ConnectUserButtonProps, ConnectProfileModalProps } from "./compone
|
|
|
32
32
|
export { ConnectAccountSessions } from "./components/account-sessions.js";
|
|
33
33
|
export type { ConnectAccountSessionsProps } from "./components/account-sessions.js";
|
|
34
34
|
export type { ConnectUserMenuProps } from "./components/user-menu.js";
|
|
35
|
+
export { connectPortalUrl, useConnectPortalUrl } from "./portal-links.js";
|
|
35
36
|
export { AppShell } from "./components/app-shell.js";
|
|
36
37
|
export type { AppShellProps } from "./components/app-shell.js";
|
|
37
38
|
export { ConnectSidebar, ConnectSidebarHeader, ConnectSidebarBody, ConnectSidebarFooter, ConnectSidebarRow, } from "./components/sidebar.js";
|
|
@@ -104,5 +105,9 @@ export { useConnectWorkspaces, useConnectWorkspaceMembers } from "./provider.js"
|
|
|
104
105
|
export { ConnectBookingCalendar } from "./components/booking-calendar.js";
|
|
105
106
|
export type { ConnectBookingCalendarProps } from "./components/booking-calendar.js";
|
|
106
107
|
export { ConnectBookingDialog, ConnectBookingDetail } from "./components/booking-dialogs.js";
|
|
107
|
-
export {
|
|
108
|
+
export { ConnectAvailabilityFormModal } from "./components/availability-form.js";
|
|
109
|
+
export type { ConnectAvailabilityFormModalProps } from "./components/availability-form.js";
|
|
110
|
+
export { ConnectEventTypeFormModal } from "./components/event-type-form.js";
|
|
111
|
+
export type { ConnectEventTypeFormModalProps } from "./components/event-type-form.js";
|
|
112
|
+
export { useBookableEventTypes, useAvailabilitySchedules, useCreateAvailabilitySchedule, useCreateEventType, useAvailableSlots, useBookings, useBookingActions, type SlotQuery, type BookingsQuery, } from "./use-scheduling.js";
|
|
108
113
|
export { layoutBookings, dayColumns, zonedInstant, type GridWindow, type PositionedBooking, } from "./components/booking-grid.js";
|