@cosmoops/connect-sdk 0.38.0 → 0.40.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 +37 -0
- package/dist/cjs/client/components/availability-form.js +52 -24
- package/dist/cjs/client/components/create-workspace-form.js +66 -0
- package/dist/cjs/client/components/event-type-form.js +99 -25
- package/dist/cjs/client/components/workspace-switcher.js +42 -9
- package/dist/cjs/client/index.js +11 -1
- package/dist/cjs/client/use-scheduling.js +37 -0
- package/dist/cjs/client/use-workspaces.js +41 -0
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +103 -2
- package/dist/cjs/server/resources/scheduling.js +67 -0
- package/dist/cjs/server/resources/workspaces.js +30 -11
- package/dist/esm/client/components/availability-form.js +53 -25
- package/dist/esm/client/components/create-workspace-form.js +63 -0
- package/dist/esm/client/components/event-type-form.js +100 -26
- package/dist/esm/client/components/workspace-switcher.js +42 -9
- package/dist/esm/client/index.js +6 -1
- package/dist/esm/client/use-scheduling.js +34 -0
- package/dist/esm/client/use-workspaces.js +38 -0
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +103 -2
- package/dist/esm/server/resources/scheduling.js +67 -0
- package/dist/esm/server/resources/workspaces.js +30 -11
- package/dist/types/client/components/availability-form.d.ts +23 -12
- package/dist/types/client/components/create-workspace-form.d.ts +32 -0
- package/dist/types/client/components/event-type-form.d.ts +30 -8
- package/dist/types/client/components/workspace-switcher.d.ts +9 -9
- package/dist/types/client/index.d.ts +5 -1
- package/dist/types/client/use-scheduling.d.ts +46 -2
- package/dist/types/client/use-workspaces.d.ts +27 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/scheduling.d.ts +66 -2
- package/dist/types/server/resources/workspaces.d.ts +28 -0
- package/dist/types/types.d.ts +33 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import { Button, Input, Label, ListBox, Modal, Select, TextArea, TextField } from "@heroui/react";
|
|
5
5
|
import { useConnect, useConnectWorkspaceMembers } from "../provider.js";
|
|
6
|
-
import { useAvailabilitySchedules, useCreateEventType } from "../use-scheduling.js";
|
|
6
|
+
import { useAvailabilitySchedules, useCreateEventType, useManageableEventTypes, useUpdateEventType, } from "../use-scheduling.js";
|
|
7
7
|
const LOCATION_LABELS = {
|
|
8
8
|
phone: "Phone call",
|
|
9
9
|
address: "In person",
|
|
@@ -11,40 +11,113 @@ const LOCATION_LABELS = {
|
|
|
11
11
|
};
|
|
12
12
|
const LOCATION_KINDS = ["phone", "address", "link"];
|
|
13
13
|
/**
|
|
14
|
-
* Creates
|
|
15
|
-
*
|
|
14
|
+
* Creates or edits an appointment type, from inside a host application
|
|
15
|
+
* rather than a link-out to Connect.
|
|
16
16
|
*
|
|
17
17
|
* **Needs a schedule to exist first** - the picker reads `useAvailabilitySchedules`
|
|
18
18
|
* and renders "Set up availability first" with nothing to submit if none
|
|
19
19
|
* exists yet. Pair this with `ConnectAvailabilityFormModal` in a setup flow
|
|
20
20
|
* rather than assuming one.
|
|
21
21
|
*
|
|
22
|
+
* **That gate waits for the read, and the order matters.** Both bridge reads
|
|
23
|
+
* answer `[]` while they are in flight, so testing `schedules.length === 0`
|
|
24
|
+
* first showed "Set up availability first" to everybody who opened this -
|
|
25
|
+
* including, for a second or two, somebody pressing Edit on an appointment
|
|
26
|
+
* type that provably *has* a schedule. A load state wearing an empty state's
|
|
27
|
+
* words is worse than a spinner: it reads as a verdict, and on a slow
|
|
28
|
+
* connection it is the only thing the modal ever says. A failed read is named
|
|
29
|
+
* as a failure for the same reason - it is not the same fact as "none exist".
|
|
30
|
+
*
|
|
22
31
|
* **Host is a specific member, never a team.** There is no groups read in
|
|
23
32
|
* this bridge, so a round-robin team host stays a Console-only capability -
|
|
24
|
-
* defaults to whoever is signed in, with an optional picker from
|
|
25
|
-
* workspace's own roster.
|
|
33
|
+
* defaults to whoever is signed in on create, with an optional picker from
|
|
34
|
+
* the workspace's own roster. **Editing a group-hosted row is refused
|
|
35
|
+
* inline** - the modal shows a notice instead of a form, because this
|
|
36
|
+
* surface cannot represent (and so cannot safely resend) a team host.
|
|
37
|
+
*
|
|
38
|
+
* This outer component only decides *which* message or form to show; the
|
|
39
|
+
* form itself is `EventTypeFields` below, mounted with `key={editing?.id}` -
|
|
40
|
+
* see that component's own doc comment for why.
|
|
26
41
|
*/
|
|
27
|
-
export function ConnectEventTypeFormModal({ isOpen, onOpenChange,
|
|
28
|
-
const
|
|
42
|
+
export function ConnectEventTypeFormModal({ isOpen, onOpenChange, eventTypeId = null, onSaved, }) {
|
|
43
|
+
const isEdit = eventTypeId !== null;
|
|
29
44
|
const { data: membersData } = useConnectWorkspaceMembers();
|
|
30
|
-
const { schedules } = useAvailabilitySchedules();
|
|
31
|
-
const
|
|
45
|
+
const { schedules, isLoading: isLoadingSchedules, error: schedulesError, } = useAvailabilitySchedules();
|
|
46
|
+
const { eventTypes: manageableEventTypes, isLoading: isLoadingEventTypes, error: eventTypesError, } = useManageableEventTypes();
|
|
47
|
+
const members = membersData?.members ?? [];
|
|
48
|
+
const editing = isEdit ? manageableEventTypes.find((row) => row.id === eventTypeId) ?? null : null;
|
|
49
|
+
/** A group host cannot be represented (or safely resent) by this form. */
|
|
50
|
+
const isUnrepresentableHost = isEdit && editing?.host.kind === "group";
|
|
51
|
+
/*
|
|
52
|
+
* Everything this modal decides from depends on a read, so the two
|
|
53
|
+
* not-yet-answered states come first - see this component's doc comment.
|
|
54
|
+
* An edit also waits for the row itself: `editing` is `null` both while the
|
|
55
|
+
* list is loading and when it has arrived without this id, and only the
|
|
56
|
+
* second of those is a real answer.
|
|
57
|
+
*/
|
|
58
|
+
const isLoading = isLoadingSchedules || (isEdit && (isLoadingEventTypes || (!editing && !eventTypesError)));
|
|
59
|
+
const readError = schedulesError ?? (isEdit ? eventTypesError : null);
|
|
60
|
+
let body;
|
|
61
|
+
if (isLoading) {
|
|
62
|
+
body = _jsx("p", { className: "text-sm text-muted", children: "Loading\u2026" });
|
|
63
|
+
}
|
|
64
|
+
else if (readError) {
|
|
65
|
+
body = (_jsx("p", { className: "text-sm text-muted", children: "That could not be loaded just now. Close this and try again." }));
|
|
66
|
+
}
|
|
67
|
+
else if (isUnrepresentableHost) {
|
|
68
|
+
body = (_jsx("p", { className: "text-sm text-muted", children: "This appointment type is hosted by a team, which this screen cannot show or change. Edit it in Connect." }));
|
|
69
|
+
}
|
|
70
|
+
else if (schedules.length === 0) {
|
|
71
|
+
body = (_jsx("p", { className: "text-sm text-muted", children: "Set up availability first - an appointment type needs working hours to book against." }));
|
|
72
|
+
}
|
|
73
|
+
else if (isEdit && !editing) {
|
|
74
|
+
/*
|
|
75
|
+
* The list arrived and does not hold this id - deleted, or never visible
|
|
76
|
+
* to this caller. Distinct from the load above, which is why that one
|
|
77
|
+
* cannot be allowed to borrow these words either.
|
|
78
|
+
*/
|
|
79
|
+
body = _jsx("p", { className: "text-sm text-muted", children: "That appointment type no longer exists." });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
body = (_jsx(EventTypeFields, { editing: editing, schedules: schedules, members: members, onCancel: () => onOpenChange(false), onSaved: (saved) => {
|
|
83
|
+
onSaved?.(saved);
|
|
84
|
+
onOpenChange(false);
|
|
85
|
+
} }, editing?.id ?? "create"));
|
|
86
|
+
}
|
|
87
|
+
return (_jsx(Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { children: _jsxs(Modal.Dialog, { className: "sm:max-w-140", children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: isEdit ? "Edit appointment type" : "New appointment type" }) }), _jsx(Modal.Body, { children: body })] }) }) }) }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The form itself, mounted fresh (via the parent's `key`) for each distinct
|
|
91
|
+
* record it edits - or once for create. That remount is what initializes
|
|
92
|
+
* every field straight from `editing` with a plain `useState(initial)`
|
|
93
|
+
* rather than a `useEffect` copying it in after the fact: `editing` arrives
|
|
94
|
+
* asynchronously (`useManageableEventTypes` is a bridge read), and the
|
|
95
|
+
* parent already withholds this component until it has landed, so there is
|
|
96
|
+
* no later moment for a field to overwrite a half-typed edit.
|
|
97
|
+
*/
|
|
98
|
+
function EventTypeFields({ editing, schedules, members, onCancel, onSaved, }) {
|
|
99
|
+
const isEdit = editing !== null;
|
|
100
|
+
const { session } = useConnect();
|
|
32
101
|
const defaultHostId = useMemo(() => {
|
|
33
102
|
const self = members.find((member) => member.userId === session?.user?.userId);
|
|
34
103
|
return self?.memberId ?? members[0]?.memberId ?? "";
|
|
35
104
|
}, [members, session?.user?.userId]);
|
|
36
|
-
const [name, setName] = useState("");
|
|
37
|
-
const [description, setDescription] = useState("");
|
|
38
|
-
const [durationMinutes, setDurationMinutes] = useState(30);
|
|
39
|
-
const [bufferBefore, setBufferBefore] = useState(5);
|
|
40
|
-
const [bufferAfter, setBufferAfter] = useState(5);
|
|
41
|
-
const [scheduleId, setScheduleId] = useState("");
|
|
42
|
-
const [hostMemberId, setHostMemberId] = useState("");
|
|
43
|
-
const [locationKind, setLocationKind] = useState("phone");
|
|
44
|
-
const [locationValue, setLocationValue] = useState("");
|
|
105
|
+
const [name, setName] = useState(editing?.name ?? "");
|
|
106
|
+
const [description, setDescription] = useState(editing?.description ?? "");
|
|
107
|
+
const [durationMinutes, setDurationMinutes] = useState(editing?.durationMinutes ?? 30);
|
|
108
|
+
const [bufferBefore, setBufferBefore] = useState(editing?.bufferBeforeMinutes ?? 5);
|
|
109
|
+
const [bufferAfter, setBufferAfter] = useState(editing?.bufferAfterMinutes ?? 5);
|
|
110
|
+
const [scheduleId, setScheduleId] = useState(editing?.scheduleId ?? "");
|
|
111
|
+
const [hostMemberId, setHostMemberId] = useState(editing?.host.kind === "member" ? editing.host.memberId : "");
|
|
112
|
+
const [locationKind, setLocationKind] = useState(editing?.location.kind ?? "phone");
|
|
113
|
+
const [locationValue, setLocationValue] = useState(editing?.location.value ?? "");
|
|
45
114
|
const effectiveScheduleId = scheduleId || schedules[0]?.id || "";
|
|
46
115
|
const effectiveHostMemberId = hostMemberId || defaultHostId;
|
|
47
|
-
const { createEventType, isPending, error, clearError } = useCreateEventType();
|
|
116
|
+
const { createEventType, isPending: isCreating, error: createError, clearError: clearCreateError } = useCreateEventType();
|
|
117
|
+
const { updateEventType, isPending: isUpdating, error: updateError, clearError: clearUpdateError } = useUpdateEventType();
|
|
118
|
+
const isPending = isCreating || isUpdating;
|
|
119
|
+
const error = isEdit ? updateError : createError;
|
|
120
|
+
const clearError = isEdit ? clearUpdateError : clearCreateError;
|
|
48
121
|
const isFormValid = name.trim().length > 0 &&
|
|
49
122
|
durationMinutes >= 5 &&
|
|
50
123
|
durationMinutes <= 480 &&
|
|
@@ -57,7 +130,7 @@ export function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, })
|
|
|
57
130
|
return;
|
|
58
131
|
clearError();
|
|
59
132
|
const trimmedDescription = description.trim();
|
|
60
|
-
const
|
|
133
|
+
const shared = {
|
|
61
134
|
name: name.trim(),
|
|
62
135
|
description: trimmedDescription.length > 0 ? trimmedDescription : null,
|
|
63
136
|
durationMinutes,
|
|
@@ -69,11 +142,12 @@ export function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, })
|
|
|
69
142
|
kind: locationKind,
|
|
70
143
|
value: locationValue.trim().length > 0 ? locationValue.trim() : null,
|
|
71
144
|
},
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
145
|
+
};
|
|
146
|
+
const saved = isEdit
|
|
147
|
+
? await updateEventType({ eventTypeId: editing.id, ...shared })
|
|
148
|
+
: await createEventType(shared);
|
|
149
|
+
if (saved)
|
|
150
|
+
onSaved(saved);
|
|
77
151
|
};
|
|
78
|
-
return (
|
|
152
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs(TextField, { isRequired: true, variant: "secondary", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { placeholder: "Consultation", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), _jsxs(TextField, { variant: "secondary", children: [_jsx(Label, { children: "Description" }), _jsx(TextArea, { placeholder: "What this appointment is for", value: description, onChange: (e) => setDescription(e.currentTarget.value), disabled: isPending, rows: 2 })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs(TextField, { isRequired: true, variant: "secondary", children: [_jsx(Label, { children: "Length (min)" }), _jsx(Input, { type: "number", value: String(durationMinutes), onChange: (e) => setDurationMinutes(Number(e.currentTarget.value)), disabled: isPending })] }), _jsxs(TextField, { variant: "secondary", children: [_jsx(Label, { children: "Gap before" }), _jsx(Input, { type: "number", value: String(bufferBefore), onChange: (e) => setBufferBefore(Number(e.currentTarget.value)), disabled: isPending })] }), _jsxs(TextField, { variant: "secondary", children: [_jsx(Label, { children: "Gap after" }), _jsx(Input, { type: "number", value: String(bufferAfter), onChange: (e) => setBufferAfter(Number(e.currentTarget.value)), disabled: isPending })] })] }), schedules.length > 1 && (_jsxs(Select, { variant: "secondary", value: effectiveScheduleId, onChange: (value) => setScheduleId(String(value)), isDisabled: isPending, children: [_jsx(Label, { children: "Availability" }), _jsxs(Select.Trigger, { children: [_jsx(Select.Value, {}), _jsx(Select.Indicator, {})] }), _jsx(Select.Popover, { children: _jsx(ListBox, { children: schedules.map((schedule) => (_jsxs(ListBox.Item, { id: schedule.id, textValue: schedule.name, children: [schedule.name, _jsx(ListBox.ItemIndicator, {})] }, schedule.id))) }) })] })), members.length > 1 && (_jsxs(Select, { variant: "secondary", value: effectiveHostMemberId, onChange: (value) => setHostMemberId(String(value)), isDisabled: isPending, children: [_jsx(Label, { children: "Who hosts it" }), _jsxs(Select.Trigger, { children: [_jsx(Select.Value, {}), _jsx(Select.Indicator, {})] }), _jsx(Select.Popover, { children: _jsx(ListBox, { children: members.map((member) => (_jsxs(ListBox.Item, { id: member.memberId, textValue: member.displayName ?? member.email, children: [member.displayName ?? member.email, _jsx(ListBox.ItemIndicator, {})] }, member.memberId))) }) })] })), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs(Select, { variant: "secondary", value: locationKind, onChange: (value) => setLocationKind(value), isDisabled: isPending, children: [_jsx(Label, { children: "Where" }), _jsxs(Select.Trigger, { children: [_jsx(Select.Value, {}), _jsx(Select.Indicator, {})] }), _jsx(Select.Popover, { children: _jsx(ListBox, { children: LOCATION_KINDS.map((kind) => (_jsxs(ListBox.Item, { id: kind, textValue: LOCATION_LABELS[kind], children: [LOCATION_LABELS[kind], _jsx(ListBox.ItemIndicator, {})] }, kind))) }) })] }), _jsxs(TextField, { variant: "secondary", children: [_jsx(Label, { children: "Details" }), _jsx(TextArea, { placeholder: locationKind === "address" ? "Where to come" : "Left blank is fine", value: locationValue, onChange: (e) => setLocationValue(e.currentTarget.value), disabled: isPending, rows: 2 })] })] }), error && _jsx("p", { className: "text-sm text-danger", children: error }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : isEdit ? "Save changes" : "Create" })] })] }));
|
|
79
153
|
}
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { Button, Dropdown, ListBox, Separator } from "@heroui/react";
|
|
5
5
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
|
6
6
|
import { useConnect, useConnectUser, useConnectWorkspaces } from "../provider.js";
|
|
7
|
+
import { ConnectCreateWorkspaceModal } from "./create-workspace-form.js";
|
|
7
8
|
import { m } from "./motion.js";
|
|
8
9
|
import { SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
9
10
|
/** `Acme Logistics` -> `AL`. The collapsed rail's whole label for a workspace. */
|
|
@@ -11,9 +12,6 @@ function workspaceInitials(name) {
|
|
|
11
12
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
12
13
|
return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
|
|
13
14
|
}
|
|
14
|
-
function openExternal(url) {
|
|
15
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
16
|
-
}
|
|
17
15
|
/**
|
|
18
16
|
* Switches which workspace this application is looking at.
|
|
19
17
|
*
|
|
@@ -28,9 +26,9 @@ function openExternal(url) {
|
|
|
28
26
|
* a workspace is *created*, so hiding it at one workspace would guarantee
|
|
29
27
|
* nobody ever reached two. `alwaysShow` is the escape hatch for a partner
|
|
30
28
|
* application that wants that same discoverability without becoming Connect:
|
|
31
|
-
* it keeps the dropdown open below two workspaces and adds a row that
|
|
32
|
-
*
|
|
33
|
-
* that
|
|
29
|
+
* it keeps the dropdown open below two workspaces and adds a row that opens
|
|
30
|
+
* `<ConnectCreateWorkspaceModal>` inline, auto-switching into the result when
|
|
31
|
+
* that is possible - see `handleCreated` below.
|
|
34
32
|
*
|
|
35
33
|
* **Authorization is the data, not a prop.** The list comes from the caller's
|
|
36
34
|
* own session (`GET /workspaces` takes no user id), so a workspace the user
|
|
@@ -50,12 +48,13 @@ function openExternal(url) {
|
|
|
50
48
|
* `onSwitched` defaults to.
|
|
51
49
|
*/
|
|
52
50
|
export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysShow = false, }) {
|
|
53
|
-
const { mutateResource
|
|
51
|
+
const { mutateResource } = useConnect();
|
|
54
52
|
const { collapsed } = useConnectShell();
|
|
55
53
|
const user = useConnectUser();
|
|
56
|
-
const { data, isLoading } = useConnectWorkspaces();
|
|
54
|
+
const { data, isLoading, refetch } = useConnectWorkspaces();
|
|
57
55
|
const [switching, setSwitching] = useState(null);
|
|
58
56
|
const [error, setError] = useState(null);
|
|
57
|
+
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
59
58
|
const workspaces = data?.workspaces ?? [];
|
|
60
59
|
// Nothing loaded yet. Deliberately renders `null` rather than a skeleton:
|
|
61
60
|
// this sits in persistent chrome, and a placeholder that resolves to
|
|
@@ -90,11 +89,45 @@ export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, al
|
|
|
90
89
|
setSwitching(null);
|
|
91
90
|
}
|
|
92
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* After a successful create: bring the new row into this list (it is
|
|
94
|
+
* created, not switched to - see `ConnectCreateWorkspaceModal`'s doc
|
|
95
|
+
* comment), then move into it when that is actually possible.
|
|
96
|
+
*
|
|
97
|
+
* Deliberately **not** `handleSwitch` - that function's `selectable()`
|
|
98
|
+
* guard requires `subscribed`, which a workspace this young never is, so
|
|
99
|
+
* calling it here would silently do nothing. `SwitchWorkspaceUseCase`
|
|
100
|
+
* itself has no subscription gate, only membership/MFA/audience, so this
|
|
101
|
+
* calls the bridge directly and only withholds on `inAudience` - a
|
|
102
|
+
* workspace of a type this application is not sold to, which `switch`
|
|
103
|
+
* really does refuse.
|
|
104
|
+
*/
|
|
105
|
+
const handleCreated = async (workspace) => {
|
|
106
|
+
setIsCreateOpen(false);
|
|
107
|
+
refetch();
|
|
108
|
+
if (!workspace.inAudience)
|
|
109
|
+
return;
|
|
110
|
+
setSwitching(workspace.workspaceId);
|
|
111
|
+
setError(null);
|
|
112
|
+
try {
|
|
113
|
+
await mutateResource("/workspaces/switch", { workspaceId: workspace.workspaceId });
|
|
114
|
+
if (onSwitched) {
|
|
115
|
+
onSwitched(workspace);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
window.location.reload();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
setError(cause instanceof Error ? cause.message : "Workspace created, but could not switch to it");
|
|
123
|
+
setSwitching(null);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
93
126
|
return (_jsxs("div", { className: row ? "" : className, children: [_jsxs(Dropdown, { children: [row ? (
|
|
94
127
|
// A 32px tile rather than a 20px glyph, so the initials land on the
|
|
95
128
|
// same optical centre the avatar below them uses - see
|
|
96
129
|
// `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
|
|
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: () =>
|
|
130
|
+
_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: () => setIsCreateOpen(true), 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, alwaysShow ? (_jsx(ConnectCreateWorkspaceModal, { isOpen: isCreateOpen, onOpenChange: setIsCreateOpen, onCreated: (workspace) => void handleCreated(workspace) })) : null] }));
|
|
98
131
|
}
|
|
99
132
|
/** Belongs to the user, and usable here. The same two facts `switch` enforces. */
|
|
100
133
|
function selectable(workspace) {
|
package/dist/esm/client/index.js
CHANGED
|
@@ -147,6 +147,11 @@ isFreePlan,
|
|
|
147
147
|
groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
|
|
148
148
|
export { ConnectWorkspaceSwitcher } from "./components/workspace-switcher.js";
|
|
149
149
|
export { useConnectWorkspaces, useConnectWorkspaceMembers } from "./provider.js";
|
|
150
|
+
// The write half of the workspace switcher - opening another workspace under
|
|
151
|
+
// the signed-in user's own tenant, from inside a host application rather
|
|
152
|
+
// than a link-out to Connect's own "Create workspace" button.
|
|
153
|
+
export { ConnectCreateWorkspaceModal } from "./components/create-workspace-form.js";
|
|
154
|
+
export { useCreateWorkspace } from "./use-workspaces.js";
|
|
150
155
|
export { ConnectBookingCalendar } from "./components/booking-calendar.js";
|
|
151
156
|
export { ConnectBookingDialog, ConnectBookingDetail } from "./components/booking-dialogs.js";
|
|
152
157
|
// The write half of scheduling - a workspace's own first-time setup, not a
|
|
@@ -155,5 +160,5 @@ export { ConnectBookingDialog, ConnectBookingDetail } from "./components/booking
|
|
|
155
160
|
// step instead of a link-out to Connect's own Calendar settings.
|
|
156
161
|
export { ConnectAvailabilityFormModal } from "./components/availability-form.js";
|
|
157
162
|
export { ConnectEventTypeFormModal } from "./components/event-type-form.js";
|
|
158
|
-
export { useBookableEventTypes, useAvailabilitySchedules, useCreateAvailabilitySchedule, useCreateEventType, useAvailableSlots, useBookings, useBookingActions, } from "./use-scheduling.js";
|
|
163
|
+
export { useBookableEventTypes, useAvailabilitySchedules, useCreateAvailabilitySchedule, useUpdateAvailabilitySchedule, useManageableEventTypes, useCreateEventType, useUpdateEventType, useAvailableSlots, useBookings, useBookingActions, } from "./use-scheduling.js";
|
|
159
164
|
export { layoutBookings, dayColumns, zonedInstant, } from "./components/booking-grid.js";
|
|
@@ -25,6 +25,16 @@ export function useAvailabilitySchedules() {
|
|
|
25
25
|
const { data, isLoading, error } = useConnectResource("/scheduling/schedules");
|
|
26
26
|
return { schedules: data?.schedules ?? [], isLoading, error };
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The richer read `ConnectEventTypeFormModal`'s edit mode needs - everything
|
|
30
|
+
* `useBookableEventTypes`'s booking catalogue deliberately withholds. See
|
|
31
|
+
* the bridge route's own doc comment for why this is a separate read rather
|
|
32
|
+
* than a wider response on that one. Includes inactive rows.
|
|
33
|
+
*/
|
|
34
|
+
export function useManageableEventTypes() {
|
|
35
|
+
const { data, isLoading, error, refetch } = useConnectResource("/scheduling/event-types/manage");
|
|
36
|
+
return { eventTypes: data?.eventTypes ?? [], isLoading, error, refetch };
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* Gives a workspace its first working hours.
|
|
30
40
|
*
|
|
@@ -41,6 +51,16 @@ export function useCreateAvailabilitySchedule() {
|
|
|
41
51
|
createSchedule: (input) => run("/scheduling/schedules", input),
|
|
42
52
|
};
|
|
43
53
|
}
|
|
54
|
+
/** Edits an existing availability schedule. Every field optional - send only what changed. */
|
|
55
|
+
export function useUpdateAvailabilitySchedule() {
|
|
56
|
+
const { run, isPending, error, clearError } = useResourceAction();
|
|
57
|
+
return {
|
|
58
|
+
isPending,
|
|
59
|
+
error,
|
|
60
|
+
clearError,
|
|
61
|
+
updateSchedule: (input) => run("/scheduling/schedules/update", input),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
44
64
|
/**
|
|
45
65
|
* Creates the first thing a workspace can be booked for.
|
|
46
66
|
*
|
|
@@ -57,6 +77,20 @@ export function useCreateEventType() {
|
|
|
57
77
|
createEventType: (input) => run("/scheduling/event-types", input),
|
|
58
78
|
};
|
|
59
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Edits an existing appointment type. Every field optional - send only what
|
|
82
|
+
* changed. `hostMemberId` omitted leaves the host untouched, including a
|
|
83
|
+
* *group* host this surface cannot represent.
|
|
84
|
+
*/
|
|
85
|
+
export function useUpdateEventType() {
|
|
86
|
+
const { run, isPending, error, clearError } = useResourceAction();
|
|
87
|
+
return {
|
|
88
|
+
isPending,
|
|
89
|
+
error,
|
|
90
|
+
clearError,
|
|
91
|
+
updateEventType: (input) => run("/scheduling/event-types/update", input),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
60
94
|
/**
|
|
61
95
|
* Free times for one appointment type.
|
|
62
96
|
*
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { useConnect } from "./provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* Opens another workspace under the tenant the signed-in user already owns.
|
|
6
|
+
*
|
|
7
|
+
* **Not optimistic**, same posture as every write in `use-scheduling.ts`:
|
|
8
|
+
* whether the tenant's plan still has room for another workspace is a
|
|
9
|
+
* question only the server can answer, and a modal that closes before that
|
|
10
|
+
* answer comes back is the lie a two-phase write exists to avoid.
|
|
11
|
+
*
|
|
12
|
+
* The result is a `UserWorkspace` row with `isActive`/`subscribed` both
|
|
13
|
+
* `false` - creating does not switch the session to it, and the workspace has
|
|
14
|
+
* no subscription yet. Pair with `<ConnectWorkspaceSwitcher>`'s own
|
|
15
|
+
* `mutateResource("/workspaces/switch", ...)` call to move into it, guarded
|
|
16
|
+
* on `inAudience` the same way that component's `selectable()` already is.
|
|
17
|
+
*/
|
|
18
|
+
export function useCreateWorkspace() {
|
|
19
|
+
const { mutateResource } = useConnect();
|
|
20
|
+
const [isPending, setPending] = useState(false);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const createWorkspace = useCallback(async (input) => {
|
|
23
|
+
setPending(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const result = await mutateResource("/workspaces", input);
|
|
27
|
+
return result.workspace;
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
setError(cause instanceof Error ? cause.message : "Could not create that workspace.");
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
setPending(false);
|
|
35
|
+
}
|
|
36
|
+
}, [mutateResource]);
|
|
37
|
+
return { isPending, error, clearError: () => setError(null), createWorkspace };
|
|
38
|
+
}
|
|
@@ -738,8 +738,8 @@ export function createConnectHandler(options) {
|
|
|
738
738
|
*/
|
|
739
739
|
"STORE": async (request) => storeRoute(request),
|
|
740
740
|
/**
|
|
741
|
-
* The workspace switcher's
|
|
742
|
-
* session token.
|
|
741
|
+
* The workspace switcher's reads and writes, forwarded with the caller's
|
|
742
|
+
* own session token.
|
|
743
743
|
*
|
|
744
744
|
* `POST /workspaces/switch` is the only bridge route that *changes the
|
|
745
745
|
* caller's session*, so it writes the new token back through the same
|
|
@@ -751,6 +751,14 @@ export function createConnectHandler(options) {
|
|
|
751
751
|
* A partner with no `session.write` cannot re-persist a rotated token, so
|
|
752
752
|
* switching is simply absent for them rather than half-working - the same
|
|
753
753
|
* treatment `/signin` and `/callback` already get.
|
|
754
|
+
*
|
|
755
|
+
* `POST /workspaces` opens another workspace under the caller's own
|
|
756
|
+
* tenant and does not rotate anything - it does not switch the session,
|
|
757
|
+
* same as Connect's own "Create workspace" button. Every guard
|
|
758
|
+
* (ownership, the plan's workspace allowance, the business-email-domain
|
|
759
|
+
* rule) lives server-side in `CreateWorkspaceUseCase`; this route is a
|
|
760
|
+
* plain forward with the caller's session token, exactly like the reads
|
|
761
|
+
* beside it.
|
|
754
762
|
*/
|
|
755
763
|
/*
|
|
756
764
|
* Push notifications - the browser half, and deliberately only that half.
|
|
@@ -807,6 +815,20 @@ export function createConnectHandler(options) {
|
|
|
807
815
|
return json(await client.push.revokeDevice(body.id, { asUser: token }));
|
|
808
816
|
},
|
|
809
817
|
"GET /workspaces": async (request) => json({ workspaces: await client.workspaces.list({ asUser: await requireToken(request) }) }),
|
|
818
|
+
"POST /workspaces": async (request) => {
|
|
819
|
+
const token = await requireToken(request);
|
|
820
|
+
const body = (await request.json().catch(() => null));
|
|
821
|
+
if (typeof body?.name !== "string" || (body.type !== "individual" && body.type !== "business")) {
|
|
822
|
+
throw new ConnectError({
|
|
823
|
+
status: 400,
|
|
824
|
+
code: "validation_error",
|
|
825
|
+
message: "name and type are required",
|
|
826
|
+
endpoint: "POST /workspaces",
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const workspace = await client.workspaces.create({ name: body.name, type: body.type }, { asUser: token });
|
|
830
|
+
return json({ workspace });
|
|
831
|
+
},
|
|
810
832
|
"GET /workspaces/members": async (request) => json({ members: await client.workspaces.members({ asUser: await requireToken(request) }) }),
|
|
811
833
|
"POST /workspaces/switch": async (request) => {
|
|
812
834
|
const token = await requireToken(request);
|
|
@@ -890,6 +912,58 @@ export function createConnectHandler(options) {
|
|
|
890
912
|
asUser,
|
|
891
913
|
}), 201);
|
|
892
914
|
},
|
|
915
|
+
/*
|
|
916
|
+
* The richer read `ConnectEventTypeFormModal`'s edit mode needs - see
|
|
917
|
+
* `listManageableEventTypes`'s own doc comment for why this is a separate
|
|
918
|
+
* route from the catalogue above rather than a wider response on it.
|
|
919
|
+
*/
|
|
920
|
+
"GET /scheduling/event-types/manage": async (request) => {
|
|
921
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
922
|
+
const asUser = await requireToken(request);
|
|
923
|
+
return json({
|
|
924
|
+
eventTypes: await client.scheduling.listManageableEventTypes(workspaceId, { asUser }),
|
|
925
|
+
});
|
|
926
|
+
},
|
|
927
|
+
/*
|
|
928
|
+
* A fixed path with the id in the body, not `/scheduling/event-types/{id}` -
|
|
929
|
+
* this table is matched by exact string, the same reason the booking
|
|
930
|
+
* actions below take their id this way rather than as a path segment.
|
|
931
|
+
*/
|
|
932
|
+
"POST /scheduling/event-types/update": async (request) => {
|
|
933
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
934
|
+
const asUser = await requireToken(request);
|
|
935
|
+
const body = (await request.json().catch(() => null));
|
|
936
|
+
if (typeof body?.eventTypeId !== "string") {
|
|
937
|
+
throw new ConnectError({
|
|
938
|
+
status: 400,
|
|
939
|
+
code: "validation_error",
|
|
940
|
+
message: "eventTypeId is required",
|
|
941
|
+
endpoint: "/scheduling/event-types/update",
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return json(await client.scheduling.updateEventType({
|
|
945
|
+
workspaceId,
|
|
946
|
+
eventTypeId: body.eventTypeId,
|
|
947
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
948
|
+
description: typeof body.description === "string" || body.description === null
|
|
949
|
+
? body.description
|
|
950
|
+
: undefined,
|
|
951
|
+
durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : undefined,
|
|
952
|
+
bufferBeforeMinutes: typeof body.bufferBeforeMinutes === "number" ? body.bufferBeforeMinutes : undefined,
|
|
953
|
+
bufferAfterMinutes: typeof body.bufferAfterMinutes === "number" ? body.bufferAfterMinutes : undefined,
|
|
954
|
+
hostMemberId: typeof body.hostMemberId === "string" ? body.hostMemberId : undefined,
|
|
955
|
+
scheduleId: typeof body.scheduleId === "string" ? body.scheduleId : undefined,
|
|
956
|
+
location: body.location && typeof body.location.kind === "string"
|
|
957
|
+
? {
|
|
958
|
+
kind: body.location.kind,
|
|
959
|
+
value: typeof body.location.value === "string" ? body.location.value : null,
|
|
960
|
+
}
|
|
961
|
+
: undefined,
|
|
962
|
+
publicBookingEnabled: typeof body.publicBookingEnabled === "boolean" ? body.publicBookingEnabled : undefined,
|
|
963
|
+
active: typeof body.active === "boolean" ? body.active : undefined,
|
|
964
|
+
asUser,
|
|
965
|
+
}));
|
|
966
|
+
},
|
|
893
967
|
/*
|
|
894
968
|
* The working-hours templates an appointment type can point at - read
|
|
895
969
|
* before a setup UI offers to create one, so it can tell "none yet" from
|
|
@@ -928,6 +1002,33 @@ export function createConnectHandler(options) {
|
|
|
928
1002
|
asUser,
|
|
929
1003
|
}), 201);
|
|
930
1004
|
},
|
|
1005
|
+
/*
|
|
1006
|
+
* A fixed path with the id in the body, same reasoning as the event
|
|
1007
|
+
* type's own `update` route above.
|
|
1008
|
+
*/
|
|
1009
|
+
"POST /scheduling/schedules/update": async (request) => {
|
|
1010
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
1011
|
+
const asUser = await requireToken(request);
|
|
1012
|
+
const body = (await request.json().catch(() => null));
|
|
1013
|
+
if (typeof body?.scheduleId !== "string") {
|
|
1014
|
+
throw new ConnectError({
|
|
1015
|
+
status: 400,
|
|
1016
|
+
code: "validation_error",
|
|
1017
|
+
message: "scheduleId is required",
|
|
1018
|
+
endpoint: "/scheduling/schedules/update",
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return json(await client.scheduling.updateSchedule({
|
|
1022
|
+
workspaceId,
|
|
1023
|
+
scheduleId: body.scheduleId,
|
|
1024
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
1025
|
+
timezone: typeof body.timezone === "string" ? body.timezone : undefined,
|
|
1026
|
+
weeklyHours: typeof body.weeklyHours === "object" && body.weeklyHours !== null
|
|
1027
|
+
? body.weeklyHours
|
|
1028
|
+
: undefined,
|
|
1029
|
+
asUser,
|
|
1030
|
+
}));
|
|
1031
|
+
},
|
|
931
1032
|
"GET /scheduling/slots": async (request) => {
|
|
932
1033
|
const { workspaceId } = await requireWorkspace(request);
|
|
933
1034
|
const asUser = await requireToken(request);
|
|
@@ -73,6 +73,9 @@ export class SchedulingResource {
|
|
|
73
73
|
* Needs a schedule - create or pick one with `listSchedules`/`createSchedule`
|
|
74
74
|
* first. The slug for a future public booking link is derived from `name`
|
|
75
75
|
* server-side; you cannot choose one.
|
|
76
|
+
*
|
|
77
|
+
* Answers the richer *manageable* shape, not `BookableEventType` - you just
|
|
78
|
+
* created this row and are about to manage it, not book against it.
|
|
76
79
|
*/
|
|
77
80
|
createEventType(input) {
|
|
78
81
|
return request(this.transport, {
|
|
@@ -93,6 +96,70 @@ export class SchedulingResource {
|
|
|
93
96
|
headers: actingAs(input.asUser),
|
|
94
97
|
});
|
|
95
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The richer read `createEventType`/`updateEventType`'s edit mode needs -
|
|
101
|
+
* everything `listEventTypes`'s booking catalogue deliberately withholds,
|
|
102
|
+
* because this is the opposite audience: a member managing their own
|
|
103
|
+
* calendar, not a widget rendering someone else's. Includes inactive rows,
|
|
104
|
+
* since an edit screen is exactly where one gets reactivated.
|
|
105
|
+
*/
|
|
106
|
+
async listManageableEventTypes(workspaceId, options = {}) {
|
|
107
|
+
const result = await request(this.transport, {
|
|
108
|
+
method: "GET",
|
|
109
|
+
path: "/scheduling/event-types/manage",
|
|
110
|
+
query: { workspaceId },
|
|
111
|
+
headers: actingAs(options.asUser),
|
|
112
|
+
});
|
|
113
|
+
return result.eventTypes ?? [];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Edits an existing availability schedule.
|
|
117
|
+
*
|
|
118
|
+
* Narrower than what the workspace's own Calendar settings can do, the
|
|
119
|
+
* same way `createSchedule` is: `ownerMemberId` and date overrides stay
|
|
120
|
+
* untouched, since this surface never sets either.
|
|
121
|
+
*/
|
|
122
|
+
updateSchedule(input) {
|
|
123
|
+
return request(this.transport, {
|
|
124
|
+
method: "PUT",
|
|
125
|
+
path: `/scheduling/schedules/${encodeURIComponent(input.scheduleId)}`,
|
|
126
|
+
json: {
|
|
127
|
+
workspaceId: input.workspaceId,
|
|
128
|
+
name: input.name,
|
|
129
|
+
timezone: input.timezone,
|
|
130
|
+
weeklyHours: input.weeklyHours,
|
|
131
|
+
},
|
|
132
|
+
headers: actingAs(input.asUser),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Edits an existing appointment type.
|
|
137
|
+
*
|
|
138
|
+
* `hostMemberId` omitted leaves the host untouched, including a *group*
|
|
139
|
+
* host this surface cannot represent - so an edit that says nothing about
|
|
140
|
+
* the host can never accidentally overwrite a team assignment made in
|
|
141
|
+
* Connect.
|
|
142
|
+
*/
|
|
143
|
+
updateEventType(input) {
|
|
144
|
+
return request(this.transport, {
|
|
145
|
+
method: "PUT",
|
|
146
|
+
path: `/scheduling/event-types/${encodeURIComponent(input.eventTypeId)}`,
|
|
147
|
+
json: {
|
|
148
|
+
workspaceId: input.workspaceId,
|
|
149
|
+
name: input.name,
|
|
150
|
+
description: input.description,
|
|
151
|
+
durationMinutes: input.durationMinutes,
|
|
152
|
+
bufferBeforeMinutes: input.bufferBeforeMinutes,
|
|
153
|
+
bufferAfterMinutes: input.bufferAfterMinutes,
|
|
154
|
+
hostMemberId: input.hostMemberId,
|
|
155
|
+
scheduleId: input.scheduleId,
|
|
156
|
+
location: input.location,
|
|
157
|
+
publicBookingEnabled: input.publicBookingEnabled,
|
|
158
|
+
active: input.active,
|
|
159
|
+
},
|
|
160
|
+
headers: actingAs(input.asUser),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
96
163
|
/**
|
|
97
164
|
* Free times for one appointment type over one window.
|
|
98
165
|
*
|