@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs/client/components/availability-form.js +52 -24
  3. package/dist/cjs/client/components/create-workspace-form.js +66 -0
  4. package/dist/cjs/client/components/event-type-form.js +99 -25
  5. package/dist/cjs/client/components/workspace-switcher.js +42 -9
  6. package/dist/cjs/client/index.js +11 -1
  7. package/dist/cjs/client/use-scheduling.js +37 -0
  8. package/dist/cjs/client/use-workspaces.js +41 -0
  9. package/dist/cjs/server/client.js +1 -1
  10. package/dist/cjs/server/handler.js +103 -2
  11. package/dist/cjs/server/resources/scheduling.js +67 -0
  12. package/dist/cjs/server/resources/workspaces.js +30 -11
  13. package/dist/esm/client/components/availability-form.js +53 -25
  14. package/dist/esm/client/components/create-workspace-form.js +63 -0
  15. package/dist/esm/client/components/event-type-form.js +100 -26
  16. package/dist/esm/client/components/workspace-switcher.js +42 -9
  17. package/dist/esm/client/index.js +6 -1
  18. package/dist/esm/client/use-scheduling.js +34 -0
  19. package/dist/esm/client/use-workspaces.js +38 -0
  20. package/dist/esm/server/client.js +1 -1
  21. package/dist/esm/server/handler.js +103 -2
  22. package/dist/esm/server/resources/scheduling.js +67 -0
  23. package/dist/esm/server/resources/workspaces.js +30 -11
  24. package/dist/types/client/components/availability-form.d.ts +23 -12
  25. package/dist/types/client/components/create-workspace-form.d.ts +32 -0
  26. package/dist/types/client/components/event-type-form.d.ts +30 -8
  27. package/dist/types/client/components/workspace-switcher.d.ts +9 -9
  28. package/dist/types/client/index.d.ts +5 -1
  29. package/dist/types/client/use-scheduling.d.ts +46 -2
  30. package/dist/types/client/use-workspaces.d.ts +27 -0
  31. package/dist/types/server/client.d.ts +1 -1
  32. package/dist/types/server/resources/scheduling.d.ts +66 -2
  33. package/dist/types/server/resources/workspaces.d.ts +28 -0
  34. package/dist/types/types.d.ts +33 -0
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
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.40.0
7
+
8
+ ### `workspaces.create()` exists now, and the switcher's "Create workspace" row is inline
9
+
10
+ 0.37.0 added `alwaysShow` and said, deliberately, that there was no
11
+ `workspaces.create()` - creating a workspace was Connect's job, tied to
12
+ KYC/KYB and a billing profile. That reasoning held for a tenant's *first*
13
+ workspace, which still mints a tenant, an Identity Platform account and a
14
+ password reset. It never applied to a tenant's *second* workspace: opening a
15
+ branch office under a tenant the caller already owns needs none of that -
16
+ `CreateWorkspaceUseCase` just checks ownership, the tenant's plan, and the
17
+ business-email-domain rule.
18
+
19
+ So `alwaysShow`'s "Create workspace" row now opens
20
+ `<ConnectCreateWorkspaceModal>` inline - name, individual-or-business - and
21
+ switches into the result automatically when it is possible
22
+ (`workspace.inAudience`), rather than opening Connect in a new tab and
23
+ leaving the member to find their way back. Needs the new `workspaces:create`
24
+ scope on your API key.
25
+
26
+ ```tsx
27
+ import { ConnectCreateWorkspaceModal, useCreateWorkspace } from "@cosmoops/connect-sdk/client";
28
+ ```
29
+
30
+ Both are usable standalone, not only through the switcher. `useCreateWorkspace()`
31
+ is not optimistic, same as every other scheduling/workspace write here: the
32
+ tenant's plan is what decides whether another workspace is allowed at all,
33
+ and that is a question only the server can answer. There is still no
34
+ capacity read to show ahead of time - submit and show the server's own
35
+ refusal, the same posture Connect's own "Create workspace" button takes.
36
+
37
+ `ConnectWorkspaceSwitcherProps.alwaysShow`'s doc comment, and
38
+ `WorkspacesResource`'s, are both updated; nothing about the prop's name or
39
+ default changed, so an existing host that set `alwaysShow: true` for the old
40
+ link-out gets the inline flow with no code change on its side beyond the
41
+ version bump and the new scope.
42
+
6
43
  ## 0.37.0
7
44
 
8
45
  ### The workspace switcher can stay visible at one workspace, and offer a second
@@ -23,24 +23,52 @@ function timeInputToMinutes(value) {
23
23
  return (hours ?? 0) * 60 + (minutes ?? 0);
24
24
  }
25
25
  /**
26
- * Gives a workspace its first working hours, from inside a host application
27
- * rather than a link-out to Connect.
26
+ * Creates or edits a workspace's working hours, from inside a host
27
+ * application rather than a link-out to Connect.
28
28
  *
29
29
  * Deliberately narrower than Connect's own Calendar settings form: one
30
- * interval per day (no split shifts), no date overrides, and the timezone
31
- * defaults to the browser's own rather than asking. This is a first-run
32
- * setup step, not a second copy of the Console editor - "Manage in Connect"
33
- * is still where the rest of what a schedule can do lives.
30
+ * interval per day (no split shifts), no date overrides, and the timezone is
31
+ * set once at creation (from the browser's own) and shown but not editable
32
+ * here afterwards. This is a first-party setup-and-edit surface, not a
33
+ * second copy of the Console editor - "Manage in Connect" is still where the
34
+ * rest of what a schedule can do lives.
34
35
  *
35
- * **Not optimistic.** `useCreateAvailabilitySchedule` settles the write
36
- * against the server before this closes, the same posture every write in
37
- * this package takes - see `ConnectBookingDialog`.
36
+ * **Not optimistic.** The write settles against the server before this
37
+ * closes, the same posture every write in this package takes - see
38
+ * `ConnectBookingDialog`.
39
+ *
40
+ * This outer component only decides whether the row to edit has loaded yet;
41
+ * the form itself is `ScheduleFields` below, mounted with
42
+ * `key={editing?.id}` - see that component's own doc comment for why.
43
+ */
44
+ function ConnectAvailabilityFormModal({ isOpen, onOpenChange, scheduleId = null, onSaved, }) {
45
+ const isEdit = scheduleId !== null;
46
+ const { schedules, isLoading, error } = (0, use_scheduling_js_1.useAvailabilitySchedules)();
47
+ const editing = isEdit ? schedules.find((row) => row.id === scheduleId) ?? null : null;
48
+ return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-120", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: isEdit ? "Edit availability" : "Set your availability" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: isEdit && isLoading ? ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Loading\u2026" })) : isEdit && error ? ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "That could not be loaded just now. Close this and try again." })) : isEdit && !editing ? ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Those working hours no longer exist." })) : ((0, jsx_runtime_1.jsx)(ScheduleFields, { editing: editing, onCancel: () => onOpenChange(false), onSaved: (saved) => {
49
+ onSaved?.(saved);
50
+ onOpenChange(false);
51
+ } }, editing?.id ?? "create")) })] }) }) }) }));
52
+ }
53
+ /**
54
+ * The form itself, mounted fresh (via the parent's `key`) for each distinct
55
+ * record it edits - or once for create. That remount is what initializes
56
+ * every field straight from `editing` with a plain `useState(initial)`
57
+ * rather than a `useEffect` copying it in after the fact: `editing` arrives
58
+ * asynchronously (`useAvailabilitySchedules` is a bridge read), and the
59
+ * parent already withholds this component until it has landed, so there is
60
+ * no later moment for a field to overwrite a half-typed edit.
38
61
  */
39
- function ConnectAvailabilityFormModal({ isOpen, onOpenChange, onCreated, }) {
40
- const [name, setName] = (0, react_1.useState)("Working hours");
41
- const [timezone] = (0, react_1.useState)(() => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
42
- const [weeklyHours, setWeeklyHours] = (0, react_1.useState)(defaultWeeklyHours);
43
- const { createSchedule, isPending, error, clearError } = (0, use_scheduling_js_1.useCreateAvailabilitySchedule)();
62
+ function ScheduleFields({ editing, onCancel, onSaved, }) {
63
+ const isEdit = editing !== null;
64
+ const [name, setName] = (0, react_1.useState)(editing?.name ?? "Working hours");
65
+ const [timezone] = (0, react_1.useState)(() => editing?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC");
66
+ const [weeklyHours, setWeeklyHours] = (0, react_1.useState)(editing?.weeklyHours ?? defaultWeeklyHours);
67
+ const { createSchedule, isPending: isCreating, error: createError, clearError: clearCreateError } = (0, use_scheduling_js_1.useCreateAvailabilitySchedule)();
68
+ const { updateSchedule, isPending: isUpdating, error: updateError, clearError: clearUpdateError } = (0, use_scheduling_js_1.useUpdateAvailabilitySchedule)();
69
+ const isPending = isCreating || isUpdating;
70
+ const error = isEdit ? updateError : createError;
71
+ const clearError = isEdit ? clearUpdateError : clearCreateError;
44
72
  const toggleDay = (day, open) => {
45
73
  setWeeklyHours((prev) => ({
46
74
  ...prev,
@@ -61,15 +89,15 @@ function ConnectAvailabilityFormModal({ isOpen, onOpenChange, onCreated, }) {
61
89
  if (!isFormValid)
62
90
  return;
63
91
  clearError();
64
- const schedule = await createSchedule({ name: name.trim(), timezone, weeklyHours });
65
- if (schedule) {
66
- onCreated?.(schedule);
67
- onOpenChange(false);
68
- }
92
+ const saved = isEdit
93
+ ? await updateSchedule({ scheduleId: editing.id, name: name.trim(), weeklyHours })
94
+ : await createSchedule({ name: name.trim(), timezone, weeklyHours });
95
+ if (saved)
96
+ onSaved(saved);
69
97
  };
70
- return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-120", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "Set your availability" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Timezone: ", timezone] }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2", children: WEEKDAY_KEYS.map((day) => {
71
- const interval = weeklyHours[day][0];
72
- const isOpenDay = interval !== undefined;
73
- return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(react_2.Switch, { isSelected: isOpenDay, onChange: (open) => toggleDay(day, open), isDisabled: isPending, children: (0, jsx_runtime_1.jsxs)(react_2.Switch.Content, { children: [(0, jsx_runtime_1.jsx)(react_2.Switch.Control, { children: (0, jsx_runtime_1.jsx)(react_2.Switch.Thumb, {}) }), (0, jsx_runtime_1.jsx)("span", { className: "w-24 shrink-0 text-sm", children: WEEKDAY_LABELS[day] })] }) }), isOpenDay && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.startMinute), onChange: (e) => setDayTime(day, "startMinute", e.currentTarget.value), disabled: isPending }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs text-muted", children: "to" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.endMinute), onChange: (e) => setDayTime(day, "endMinute", e.currentTarget.value), disabled: isPending })] }))] }, day));
74
- }) }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: () => onOpenChange(false), isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : "Save" })] })] }) })] }) }) }) }));
98
+ return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Timezone: ", timezone] }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-2", children: WEEKDAY_KEYS.map((day) => {
99
+ const interval = weeklyHours[day][0];
100
+ const isOpenDay = interval !== undefined;
101
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(react_2.Switch, { isSelected: isOpenDay, onChange: (open) => toggleDay(day, open), isDisabled: isPending, children: (0, jsx_runtime_1.jsxs)(react_2.Switch.Content, { children: [(0, jsx_runtime_1.jsx)(react_2.Switch.Control, { children: (0, jsx_runtime_1.jsx)(react_2.Switch.Thumb, {}) }), (0, jsx_runtime_1.jsx)("span", { className: "w-24 shrink-0 text-sm", children: WEEKDAY_LABELS[day] })] }) }), isOpenDay && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.startMinute), onChange: (e) => setDayTime(day, "startMinute", e.currentTarget.value), disabled: isPending }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs text-muted", children: "to" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "time", value: minutesToTimeInput(interval.endMinute), onChange: (e) => setDayTime(day, "endMinute", e.currentTarget.value), disabled: isPending })] }))] }, day));
102
+ }) }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : isEdit ? "Save changes" : "Save" })] })] }));
75
103
  }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectCreateWorkspaceModal = ConnectCreateWorkspaceModal;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const use_workspaces_js_1 = require("../use-workspaces.js");
9
+ const WORKSPACE_TYPES = [
10
+ {
11
+ id: "business",
12
+ label: "Business",
13
+ hint: "A company. Verified with company KYB, and can subscribe to anything sold to businesses.",
14
+ },
15
+ {
16
+ id: "individual",
17
+ label: "Individual",
18
+ hint: "A person. Verified against your own identity rather than a company.",
19
+ },
20
+ ];
21
+ /**
22
+ * Opens another workspace under the signed-in user's own tenant - a branch
23
+ * office, a second business unit - from inside a host application rather
24
+ * than a link-out to Connect.
25
+ *
26
+ * **Only the tenant's owner can use this successfully.** `CreateWorkspaceUseCase`
27
+ * refuses anyone who merely belongs to a colleague's tenant, with a message
28
+ * naming that - someone who wants a workspace of their own uses a personal
29
+ * account instead.
30
+ *
31
+ * **The tenant's plan decides how many workspaces it may hold, and this does
32
+ * not pre-check it.** There is no capacity read to show ahead of time - the
33
+ * same posture Connect's own "Create workspace" button takes - so a refusal
34
+ * surfaces here exactly as the server wrote it ("Your current plan covers a
35
+ * single workspace - subscribe to a plan that includes additional workspaces
36
+ * to open another.", or "You've used all 3 of the workspaces your plans
37
+ * include. Upgrade a plan or delete a workspace to open another.").
38
+ *
39
+ * **Does not switch the session to the new workspace.** Creating one never
40
+ * does, on Connect's own screen either - see `useCreateWorkspace`'s doc
41
+ * comment for how to chain a switch once you have the result, and guard that
42
+ * chain on `inAudience` the same way `<ConnectWorkspaceSwitcher>` already
43
+ * does for every other row.
44
+ */
45
+ function ConnectCreateWorkspaceModal({ isOpen, onOpenChange, onCreated, }) {
46
+ return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-110", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "Create workspace" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: (0, jsx_runtime_1.jsx)(CreateWorkspaceFields, { onCancel: () => onOpenChange(false), onCreated: (workspace) => {
47
+ onCreated?.(workspace);
48
+ onOpenChange(false);
49
+ } }) })] }) }) }) }));
50
+ }
51
+ function CreateWorkspaceFields({ onCancel, onCreated, }) {
52
+ const [name, setName] = (0, react_1.useState)("");
53
+ const [type, setType] = (0, react_1.useState)("business");
54
+ const { createWorkspace, isPending, error, clearError } = (0, use_workspaces_js_1.useCreateWorkspace)();
55
+ const isFormValid = name.trim().length > 0 && !isPending;
56
+ const handleSubmit = async (e) => {
57
+ e.preventDefault();
58
+ if (!isFormValid)
59
+ return;
60
+ clearError();
61
+ const created = await createWorkspace({ name: name.trim(), type });
62
+ if (created)
63
+ onCreated(created);
64
+ };
65
+ return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: "Acme Logistics", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)(react_2.RadioGroup, { variant: "secondary", value: type, onChange: (value) => setType(value), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Type" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-1.5 flex flex-col gap-2", children: WORKSPACE_TYPES.map((option) => ((0, jsx_runtime_1.jsx)(react_2.Radio, { value: option.id, children: (0, jsx_runtime_1.jsxs)(react_2.Radio.Content, { children: [(0, jsx_runtime_1.jsx)(react_2.Radio.Control, { children: (0, jsx_runtime_1.jsx)(react_2.Radio.Indicator, {}) }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium", children: option.label }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: option.hint })] })] }) }, option.id))) })] }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Creating…" : "Create" })] })] }));
66
+ }
@@ -14,40 +14,113 @@ const LOCATION_LABELS = {
14
14
  };
15
15
  const LOCATION_KINDS = ["phone", "address", "link"];
16
16
  /**
17
- * Creates the first thing a workspace can be booked for, from inside a host
18
- * application rather than a link-out to Connect.
17
+ * Creates or edits an appointment type, from inside a host application
18
+ * rather than a link-out to Connect.
19
19
  *
20
20
  * **Needs a schedule to exist first** - the picker reads `useAvailabilitySchedules`
21
21
  * and renders "Set up availability first" with nothing to submit if none
22
22
  * exists yet. Pair this with `ConnectAvailabilityFormModal` in a setup flow
23
23
  * rather than assuming one.
24
24
  *
25
+ * **That gate waits for the read, and the order matters.** Both bridge reads
26
+ * answer `[]` while they are in flight, so testing `schedules.length === 0`
27
+ * first showed "Set up availability first" to everybody who opened this -
28
+ * including, for a second or two, somebody pressing Edit on an appointment
29
+ * type that provably *has* a schedule. A load state wearing an empty state's
30
+ * words is worse than a spinner: it reads as a verdict, and on a slow
31
+ * connection it is the only thing the modal ever says. A failed read is named
32
+ * as a failure for the same reason - it is not the same fact as "none exist".
33
+ *
25
34
  * **Host is a specific member, never a team.** There is no groups read in
26
35
  * this bridge, so a round-robin team host stays a Console-only capability -
27
- * defaults to whoever is signed in, with an optional picker from the
28
- * workspace's own roster.
36
+ * defaults to whoever is signed in on create, with an optional picker from
37
+ * the workspace's own roster. **Editing a group-hosted row is refused
38
+ * inline** - the modal shows a notice instead of a form, because this
39
+ * surface cannot represent (and so cannot safely resend) a team host.
40
+ *
41
+ * This outer component only decides *which* message or form to show; the
42
+ * form itself is `EventTypeFields` below, mounted with `key={editing?.id}` -
43
+ * see that component's own doc comment for why.
29
44
  */
30
- function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, }) {
31
- const { session } = (0, provider_js_1.useConnect)();
45
+ function ConnectEventTypeFormModal({ isOpen, onOpenChange, eventTypeId = null, onSaved, }) {
46
+ const isEdit = eventTypeId !== null;
32
47
  const { data: membersData } = (0, provider_js_1.useConnectWorkspaceMembers)();
33
- const { schedules } = (0, use_scheduling_js_1.useAvailabilitySchedules)();
34
- const members = (0, react_1.useMemo)(() => membersData?.members ?? [], [membersData]);
48
+ const { schedules, isLoading: isLoadingSchedules, error: schedulesError, } = (0, use_scheduling_js_1.useAvailabilitySchedules)();
49
+ const { eventTypes: manageableEventTypes, isLoading: isLoadingEventTypes, error: eventTypesError, } = (0, use_scheduling_js_1.useManageableEventTypes)();
50
+ const members = membersData?.members ?? [];
51
+ const editing = isEdit ? manageableEventTypes.find((row) => row.id === eventTypeId) ?? null : null;
52
+ /** A group host cannot be represented (or safely resent) by this form. */
53
+ const isUnrepresentableHost = isEdit && editing?.host.kind === "group";
54
+ /*
55
+ * Everything this modal decides from depends on a read, so the two
56
+ * not-yet-answered states come first - see this component's doc comment.
57
+ * An edit also waits for the row itself: `editing` is `null` both while the
58
+ * list is loading and when it has arrived without this id, and only the
59
+ * second of those is a real answer.
60
+ */
61
+ const isLoading = isLoadingSchedules || (isEdit && (isLoadingEventTypes || (!editing && !eventTypesError)));
62
+ const readError = schedulesError ?? (isEdit ? eventTypesError : null);
63
+ let body;
64
+ if (isLoading) {
65
+ body = (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Loading\u2026" });
66
+ }
67
+ else if (readError) {
68
+ body = ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "That could not be loaded just now. Close this and try again." }));
69
+ }
70
+ else if (isUnrepresentableHost) {
71
+ body = ((0, jsx_runtime_1.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." }));
72
+ }
73
+ else if (schedules.length === 0) {
74
+ body = ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Set up availability first - an appointment type needs working hours to book against." }));
75
+ }
76
+ else if (isEdit && !editing) {
77
+ /*
78
+ * The list arrived and does not hold this id - deleted, or never visible
79
+ * to this caller. Distinct from the load above, which is why that one
80
+ * cannot be allowed to borrow these words either.
81
+ */
82
+ body = (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "That appointment type no longer exists." });
83
+ }
84
+ else {
85
+ body = ((0, jsx_runtime_1.jsx)(EventTypeFields, { editing: editing, schedules: schedules, members: members, onCancel: () => onOpenChange(false), onSaved: (saved) => {
86
+ onSaved?.(saved);
87
+ onOpenChange(false);
88
+ } }, editing?.id ?? "create"));
89
+ }
90
+ return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-140", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: isEdit ? "Edit appointment type" : "New appointment type" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: body })] }) }) }) }));
91
+ }
92
+ /**
93
+ * The form itself, mounted fresh (via the parent's `key`) for each distinct
94
+ * record it edits - or once for create. That remount is what initializes
95
+ * every field straight from `editing` with a plain `useState(initial)`
96
+ * rather than a `useEffect` copying it in after the fact: `editing` arrives
97
+ * asynchronously (`useManageableEventTypes` is a bridge read), and the
98
+ * parent already withholds this component until it has landed, so there is
99
+ * no later moment for a field to overwrite a half-typed edit.
100
+ */
101
+ function EventTypeFields({ editing, schedules, members, onCancel, onSaved, }) {
102
+ const isEdit = editing !== null;
103
+ const { session } = (0, provider_js_1.useConnect)();
35
104
  const defaultHostId = (0, react_1.useMemo)(() => {
36
105
  const self = members.find((member) => member.userId === session?.user?.userId);
37
106
  return self?.memberId ?? members[0]?.memberId ?? "";
38
107
  }, [members, session?.user?.userId]);
39
- const [name, setName] = (0, react_1.useState)("");
40
- const [description, setDescription] = (0, react_1.useState)("");
41
- const [durationMinutes, setDurationMinutes] = (0, react_1.useState)(30);
42
- const [bufferBefore, setBufferBefore] = (0, react_1.useState)(5);
43
- const [bufferAfter, setBufferAfter] = (0, react_1.useState)(5);
44
- const [scheduleId, setScheduleId] = (0, react_1.useState)("");
45
- const [hostMemberId, setHostMemberId] = (0, react_1.useState)("");
46
- const [locationKind, setLocationKind] = (0, react_1.useState)("phone");
47
- const [locationValue, setLocationValue] = (0, react_1.useState)("");
108
+ const [name, setName] = (0, react_1.useState)(editing?.name ?? "");
109
+ const [description, setDescription] = (0, react_1.useState)(editing?.description ?? "");
110
+ const [durationMinutes, setDurationMinutes] = (0, react_1.useState)(editing?.durationMinutes ?? 30);
111
+ const [bufferBefore, setBufferBefore] = (0, react_1.useState)(editing?.bufferBeforeMinutes ?? 5);
112
+ const [bufferAfter, setBufferAfter] = (0, react_1.useState)(editing?.bufferAfterMinutes ?? 5);
113
+ const [scheduleId, setScheduleId] = (0, react_1.useState)(editing?.scheduleId ?? "");
114
+ const [hostMemberId, setHostMemberId] = (0, react_1.useState)(editing?.host.kind === "member" ? editing.host.memberId : "");
115
+ const [locationKind, setLocationKind] = (0, react_1.useState)(editing?.location.kind ?? "phone");
116
+ const [locationValue, setLocationValue] = (0, react_1.useState)(editing?.location.value ?? "");
48
117
  const effectiveScheduleId = scheduleId || schedules[0]?.id || "";
49
118
  const effectiveHostMemberId = hostMemberId || defaultHostId;
50
- const { createEventType, isPending, error, clearError } = (0, use_scheduling_js_1.useCreateEventType)();
119
+ const { createEventType, isPending: isCreating, error: createError, clearError: clearCreateError } = (0, use_scheduling_js_1.useCreateEventType)();
120
+ const { updateEventType, isPending: isUpdating, error: updateError, clearError: clearUpdateError } = (0, use_scheduling_js_1.useUpdateEventType)();
121
+ const isPending = isCreating || isUpdating;
122
+ const error = isEdit ? updateError : createError;
123
+ const clearError = isEdit ? clearUpdateError : clearCreateError;
51
124
  const isFormValid = name.trim().length > 0 &&
52
125
  durationMinutes >= 5 &&
53
126
  durationMinutes <= 480 &&
@@ -60,7 +133,7 @@ function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, }) {
60
133
  return;
61
134
  clearError();
62
135
  const trimmedDescription = description.trim();
63
- const created = await createEventType({
136
+ const shared = {
64
137
  name: name.trim(),
65
138
  description: trimmedDescription.length > 0 ? trimmedDescription : null,
66
139
  durationMinutes,
@@ -72,11 +145,12 @@ function ConnectEventTypeFormModal({ isOpen, onOpenChange, onCreated, }) {
72
145
  kind: locationKind,
73
146
  value: locationValue.trim().length > 0 ? locationValue.trim() : null,
74
147
  },
75
- });
76
- if (created) {
77
- onCreated?.(created);
78
- onOpenChange(false);
79
- }
148
+ };
149
+ const saved = isEdit
150
+ ? await updateEventType({ eventTypeId: editing.id, ...shared })
151
+ : await createEventType(shared);
152
+ if (saved)
153
+ onSaved(saved);
80
154
  };
81
- return ((0, jsx_runtime_1.jsx)(react_2.Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: (0, jsx_runtime_1.jsx)(react_2.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.Modal.Dialog, { className: "sm:max-w-140", children: [(0, jsx_runtime_1.jsx)(react_2.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Modal.Heading, { children: "New appointment type" }) }), (0, jsx_runtime_1.jsx)(react_2.Modal.Body, { children: schedules.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Set up availability first - an appointment type needs working hours to book against." })) : ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: "Consultation", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Description" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: "What this appointment is for", value: description, onChange: (e) => setDescription(e.currentTarget.value), disabled: isPending, rows: 2 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Length (min)" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(durationMinutes), onChange: (e) => setDurationMinutes(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap before" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferBefore), onChange: (e) => setBufferBefore(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap after" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferAfter), onChange: (e) => setBufferAfter(Number(e.currentTarget.value)), disabled: isPending })] })] }), schedules.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveScheduleId, onChange: (value) => setScheduleId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Availability" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: schedules.map((schedule) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: schedule.id, textValue: schedule.name, children: [schedule.name, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, schedule.id))) }) })] })), members.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveHostMemberId, onChange: (value) => setHostMemberId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Who hosts it" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: members.map((member) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: member.memberId, textValue: member.displayName ?? member.email, children: [member.displayName ?? member.email, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, member.memberId))) }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-2", children: [(0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: locationKind, onChange: (value) => setLocationKind(value), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Where" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: LOCATION_KINDS.map((kind) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: kind, textValue: LOCATION_LABELS[kind], children: [LOCATION_LABELS[kind], (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, kind))) }) })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Details" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: locationKind === "address" ? "Where to come" : "Left blank is fine", value: locationValue, onChange: (e) => setLocationValue(e.currentTarget.value), disabled: isPending, rows: 2 })] })] }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: () => onOpenChange(false), isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : "Create" })] })] })) })] }) }) }) }));
155
+ return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Name" }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: "Consultation", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Description" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: "What this appointment is for", value: description, onChange: (e) => setDescription(e.currentTarget.value), disabled: isPending, rows: 2 })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { isRequired: true, variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Length (min)" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(durationMinutes), onChange: (e) => setDurationMinutes(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap before" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferBefore), onChange: (e) => setBufferBefore(Number(e.currentTarget.value)), disabled: isPending })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Gap after" }), (0, jsx_runtime_1.jsx)(react_2.Input, { type: "number", value: String(bufferAfter), onChange: (e) => setBufferAfter(Number(e.currentTarget.value)), disabled: isPending })] })] }), schedules.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveScheduleId, onChange: (value) => setScheduleId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Availability" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: schedules.map((schedule) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: schedule.id, textValue: schedule.name, children: [schedule.name, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, schedule.id))) }) })] })), members.length > 1 && ((0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: effectiveHostMemberId, onChange: (value) => setHostMemberId(String(value)), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Who hosts it" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: members.map((member) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: member.memberId, textValue: member.displayName ?? member.email, children: [member.displayName ?? member.email, (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, member.memberId))) }) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "grid gap-4 sm:grid-cols-2", children: [(0, jsx_runtime_1.jsxs)(react_2.Select, { variant: "secondary", value: locationKind, onChange: (value) => setLocationKind(value), isDisabled: isPending, children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Where" }), (0, jsx_runtime_1.jsxs)(react_2.Select.Trigger, { children: [(0, jsx_runtime_1.jsx)(react_2.Select.Value, {}), (0, jsx_runtime_1.jsx)(react_2.Select.Indicator, {})] }), (0, jsx_runtime_1.jsx)(react_2.Select.Popover, { children: (0, jsx_runtime_1.jsx)(react_2.ListBox, { children: LOCATION_KINDS.map((kind) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: kind, textValue: LOCATION_LABELS[kind], children: [LOCATION_LABELS[kind], (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, kind))) }) })] }), (0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { children: "Details" }), (0, jsx_runtime_1.jsx)(react_2.TextArea, { placeholder: locationKind === "address" ? "Where to come" : "Left blank is fine", value: locationValue, onChange: (e) => setLocationValue(e.currentTarget.value), disabled: isPending, rows: 2 })] })] }), error && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: error }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end gap-2", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : isEdit ? "Save changes" : "Create" })] })] }));
82
156
  }
@@ -7,6 +7,7 @@ const react_1 = require("react");
7
7
  const react_2 = require("@heroui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const provider_js_1 = require("../provider.js");
10
+ const create_workspace_form_js_1 = require("./create-workspace-form.js");
10
11
  const motion_js_1 = require("./motion.js");
11
12
  const shell_context_js_1 = require("./shell-context.js");
12
13
  /** `Acme Logistics` -> `AL`. The collapsed rail's whole label for a workspace. */
@@ -14,9 +15,6 @@ function workspaceInitials(name) {
14
15
  const parts = name.trim().split(/\s+/).filter(Boolean);
15
16
  return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
16
17
  }
17
- function openExternal(url) {
18
- window.open(url, "_blank", "noopener,noreferrer");
19
- }
20
18
  /**
21
19
  * Switches which workspace this application is looking at.
22
20
  *
@@ -31,9 +29,9 @@ function openExternal(url) {
31
29
  * a workspace is *created*, so hiding it at one workspace would guarantee
32
30
  * nobody ever reached two. `alwaysShow` is the escape hatch for a partner
33
31
  * application that wants that same discoverability without becoming Connect:
34
- * it keeps the dropdown open below two workspaces and adds a row that sends
35
- * the member to Connect to actually create one, since there is nothing here
36
- * that can.
32
+ * it keeps the dropdown open below two workspaces and adds a row that opens
33
+ * `<ConnectCreateWorkspaceModal>` inline, auto-switching into the result when
34
+ * that is possible - see `handleCreated` below.
37
35
  *
38
36
  * **Authorization is the data, not a prop.** The list comes from the caller's
39
37
  * own session (`GET /workspaces` takes no user id), so a workspace the user
@@ -53,12 +51,13 @@ function openExternal(url) {
53
51
  * `onSwitched` defaults to.
54
52
  */
55
53
  function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysShow = false, }) {
56
- const { mutateResource, appUrl } = (0, provider_js_1.useConnect)();
54
+ const { mutateResource } = (0, provider_js_1.useConnect)();
57
55
  const { collapsed } = (0, shell_context_js_1.useConnectShell)();
58
56
  const user = (0, provider_js_1.useConnectUser)();
59
- const { data, isLoading } = (0, provider_js_1.useConnectWorkspaces)();
57
+ const { data, isLoading, refetch } = (0, provider_js_1.useConnectWorkspaces)();
60
58
  const [switching, setSwitching] = (0, react_1.useState)(null);
61
59
  const [error, setError] = (0, react_1.useState)(null);
60
+ const [isCreateOpen, setIsCreateOpen] = (0, react_1.useState)(false);
62
61
  const workspaces = data?.workspaces ?? [];
63
62
  // Nothing loaded yet. Deliberately renders `null` rather than a skeleton:
64
63
  // this sits in persistent chrome, and a placeholder that resolves to
@@ -93,11 +92,45 @@ function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, alwaysSho
93
92
  setSwitching(null);
94
93
  }
95
94
  };
95
+ /**
96
+ * After a successful create: bring the new row into this list (it is
97
+ * created, not switched to - see `ConnectCreateWorkspaceModal`'s doc
98
+ * comment), then move into it when that is actually possible.
99
+ *
100
+ * Deliberately **not** `handleSwitch` - that function's `selectable()`
101
+ * guard requires `subscribed`, which a workspace this young never is, so
102
+ * calling it here would silently do nothing. `SwitchWorkspaceUseCase`
103
+ * itself has no subscription gate, only membership/MFA/audience, so this
104
+ * calls the bridge directly and only withholds on `inAudience` - a
105
+ * workspace of a type this application is not sold to, which `switch`
106
+ * really does refuse.
107
+ */
108
+ const handleCreated = async (workspace) => {
109
+ setIsCreateOpen(false);
110
+ refetch();
111
+ if (!workspace.inAudience)
112
+ return;
113
+ setSwitching(workspace.workspaceId);
114
+ setError(null);
115
+ try {
116
+ await mutateResource("/workspaces/switch", { workspaceId: workspace.workspaceId });
117
+ if (onSwitched) {
118
+ onSwitched(workspace);
119
+ }
120
+ else {
121
+ window.location.reload();
122
+ }
123
+ }
124
+ catch (cause) {
125
+ setError(cause instanceof Error ? cause.message : "Workspace created, but could not switch to it");
126
+ setSwitching(null);
127
+ }
128
+ };
96
129
  return ((0, jsx_runtime_1.jsxs)("div", { className: row ? "" : className, children: [(0, jsx_runtime_1.jsxs)(react_2.Dropdown, { children: [row ? (
97
130
  // A 32px tile rather than a 20px glyph, so the initials land on the
98
131
  // same optical centre the avatar below them uses - see
99
132
  // `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
100
- (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: (0, shell_context_js_1.sidebarRowClass)({ tile: true, className }), children: [(0, jsx_runtime_1.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) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), (0, jsx_runtime_1.jsx)(motion_js_1.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: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4" }) })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate", children: label }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), (0, jsx_runtime_1.jsx)(react_2.Dropdown.Popover, { children: (0, jsx_runtime_1.jsxs)(react_2.ListBox, { children: [workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : ((0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, workspace.workspaceId))), alwaysShow ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [workspaces.length > 0 && (0, jsx_runtime_1.jsx)(react_2.Separator, {}), (0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: "__create_workspace", textValue: "Create workspace", onAction: () => openExternal(appUrl), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: "Create workspace" })] })] })) : null] }) })] }), error && !collapsed ? (0, jsx_runtime_1.jsx)("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
133
+ (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: (0, shell_context_js_1.sidebarRowClass)({ tile: true, className }), children: [(0, jsx_runtime_1.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) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), (0, jsx_runtime_1.jsx)(motion_js_1.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: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4" }) })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate", children: label }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), (0, jsx_runtime_1.jsx)(react_2.Dropdown.Popover, { children: (0, jsx_runtime_1.jsxs)(react_2.ListBox, { children: [workspaces.map((workspace) => ((0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : ((0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), (0, jsx_runtime_1.jsx)(react_2.ListBox.ItemIndicator, {})] }, workspace.workspaceId))), alwaysShow ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [workspaces.length > 0 && (0, jsx_runtime_1.jsx)(react_2.Separator, {}), (0, jsx_runtime_1.jsxs)(react_2.ListBox.Item, { id: "__create_workspace", textValue: "Create workspace", onAction: () => setIsCreateOpen(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm", children: "Create workspace" })] })] })) : null] }) })] }), error && !collapsed ? (0, jsx_runtime_1.jsx)("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null, alwaysShow ? ((0, jsx_runtime_1.jsx)(create_workspace_form_js_1.ConnectCreateWorkspaceModal, { isOpen: isCreateOpen, onOpenChange: setIsCreateOpen, onCreated: (workspace) => void handleCreated(workspace) })) : null] }));
101
134
  }
102
135
  /** Belongs to the user, and usable here. The same two facts `switch` enforces. */
103
136
  function selectable(workspace) {
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.ConnectSideNav = exports.ConnectSidebarNav = exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.useConnectPortalUrl = exports.connectPortalUrl = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnectOptional = exports.useConnect = exports.ConnectProvider = void 0;
5
5
  exports.ConnectConfirmDialog = exports.ConnectLinkButton = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.visitorSlotOfferFrom = exports.readVisitorStream = exports.ConnectVisitorChat = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useConnectPush = exports.useActivePath = void 0;
6
- exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useCreateEventType = exports.useCreateAvailabilitySchedule = exports.useAvailabilitySchedules = exports.useBookableEventTypes = exports.ConnectEventTypeFormModal = exports.ConnectAvailabilityFormModal = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = void 0;
6
+ exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useUpdateEventType = exports.useCreateEventType = exports.useManageableEventTypes = exports.useUpdateAvailabilitySchedule = exports.useCreateAvailabilitySchedule = exports.useAvailabilitySchedules = exports.useBookableEventTypes = exports.ConnectEventTypeFormModal = exports.ConnectAvailabilityFormModal = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useCreateWorkspace = exports.ConnectCreateWorkspaceModal = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = void 0;
7
7
  /**
8
8
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
9
9
  *
@@ -264,6 +264,13 @@ Object.defineProperty(exports, "ConnectWorkspaceSwitcher", { enumerable: true, g
264
264
  var provider_js_2 = require("./provider.js");
265
265
  Object.defineProperty(exports, "useConnectWorkspaces", { enumerable: true, get: function () { return provider_js_2.useConnectWorkspaces; } });
266
266
  Object.defineProperty(exports, "useConnectWorkspaceMembers", { enumerable: true, get: function () { return provider_js_2.useConnectWorkspaceMembers; } });
267
+ // The write half of the workspace switcher - opening another workspace under
268
+ // the signed-in user's own tenant, from inside a host application rather
269
+ // than a link-out to Connect's own "Create workspace" button.
270
+ var create_workspace_form_js_1 = require("./components/create-workspace-form.js");
271
+ Object.defineProperty(exports, "ConnectCreateWorkspaceModal", { enumerable: true, get: function () { return create_workspace_form_js_1.ConnectCreateWorkspaceModal; } });
272
+ var use_workspaces_js_1 = require("./use-workspaces.js");
273
+ Object.defineProperty(exports, "useCreateWorkspace", { enumerable: true, get: function () { return use_workspaces_js_1.useCreateWorkspace; } });
267
274
  var booking_calendar_js_1 = require("./components/booking-calendar.js");
268
275
  Object.defineProperty(exports, "ConnectBookingCalendar", { enumerable: true, get: function () { return booking_calendar_js_1.ConnectBookingCalendar; } });
269
276
  var booking_dialogs_js_1 = require("./components/booking-dialogs.js");
@@ -281,7 +288,10 @@ var use_scheduling_js_1 = require("./use-scheduling.js");
281
288
  Object.defineProperty(exports, "useBookableEventTypes", { enumerable: true, get: function () { return use_scheduling_js_1.useBookableEventTypes; } });
282
289
  Object.defineProperty(exports, "useAvailabilitySchedules", { enumerable: true, get: function () { return use_scheduling_js_1.useAvailabilitySchedules; } });
283
290
  Object.defineProperty(exports, "useCreateAvailabilitySchedule", { enumerable: true, get: function () { return use_scheduling_js_1.useCreateAvailabilitySchedule; } });
291
+ Object.defineProperty(exports, "useUpdateAvailabilitySchedule", { enumerable: true, get: function () { return use_scheduling_js_1.useUpdateAvailabilitySchedule; } });
292
+ Object.defineProperty(exports, "useManageableEventTypes", { enumerable: true, get: function () { return use_scheduling_js_1.useManageableEventTypes; } });
284
293
  Object.defineProperty(exports, "useCreateEventType", { enumerable: true, get: function () { return use_scheduling_js_1.useCreateEventType; } });
294
+ Object.defineProperty(exports, "useUpdateEventType", { enumerable: true, get: function () { return use_scheduling_js_1.useUpdateEventType; } });
285
295
  Object.defineProperty(exports, "useAvailableSlots", { enumerable: true, get: function () { return use_scheduling_js_1.useAvailableSlots; } });
286
296
  Object.defineProperty(exports, "useBookings", { enumerable: true, get: function () { return use_scheduling_js_1.useBookings; } });
287
297
  Object.defineProperty(exports, "useBookingActions", { enumerable: true, get: function () { return use_scheduling_js_1.useBookingActions; } });
@@ -3,8 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.useBookableEventTypes = useBookableEventTypes;
5
5
  exports.useAvailabilitySchedules = useAvailabilitySchedules;
6
+ exports.useManageableEventTypes = useManageableEventTypes;
6
7
  exports.useCreateAvailabilitySchedule = useCreateAvailabilitySchedule;
8
+ exports.useUpdateAvailabilitySchedule = useUpdateAvailabilitySchedule;
7
9
  exports.useCreateEventType = useCreateEventType;
10
+ exports.useUpdateEventType = useUpdateEventType;
8
11
  exports.useAvailableSlots = useAvailableSlots;
9
12
  exports.useBookings = useBookings;
10
13
  exports.useBookingActions = useBookingActions;
@@ -34,6 +37,16 @@ function useAvailabilitySchedules() {
34
37
  const { data, isLoading, error } = (0, provider_js_1.useConnectResource)("/scheduling/schedules");
35
38
  return { schedules: data?.schedules ?? [], isLoading, error };
36
39
  }
40
+ /**
41
+ * The richer read `ConnectEventTypeFormModal`'s edit mode needs - everything
42
+ * `useBookableEventTypes`'s booking catalogue deliberately withholds. See
43
+ * the bridge route's own doc comment for why this is a separate read rather
44
+ * than a wider response on that one. Includes inactive rows.
45
+ */
46
+ function useManageableEventTypes() {
47
+ const { data, isLoading, error, refetch } = (0, provider_js_1.useConnectResource)("/scheduling/event-types/manage");
48
+ return { eventTypes: data?.eventTypes ?? [], isLoading, error, refetch };
49
+ }
37
50
  /**
38
51
  * Gives a workspace its first working hours.
39
52
  *
@@ -50,6 +63,16 @@ function useCreateAvailabilitySchedule() {
50
63
  createSchedule: (input) => run("/scheduling/schedules", input),
51
64
  };
52
65
  }
66
+ /** Edits an existing availability schedule. Every field optional - send only what changed. */
67
+ function useUpdateAvailabilitySchedule() {
68
+ const { run, isPending, error, clearError } = useResourceAction();
69
+ return {
70
+ isPending,
71
+ error,
72
+ clearError,
73
+ updateSchedule: (input) => run("/scheduling/schedules/update", input),
74
+ };
75
+ }
53
76
  /**
54
77
  * Creates the first thing a workspace can be booked for.
55
78
  *
@@ -66,6 +89,20 @@ function useCreateEventType() {
66
89
  createEventType: (input) => run("/scheduling/event-types", input),
67
90
  };
68
91
  }
92
+ /**
93
+ * Edits an existing appointment type. Every field optional - send only what
94
+ * changed. `hostMemberId` omitted leaves the host untouched, including a
95
+ * *group* host this surface cannot represent.
96
+ */
97
+ function useUpdateEventType() {
98
+ const { run, isPending, error, clearError } = useResourceAction();
99
+ return {
100
+ isPending,
101
+ error,
102
+ clearError,
103
+ updateEventType: (input) => run("/scheduling/event-types/update", input),
104
+ };
105
+ }
69
106
  /**
70
107
  * Free times for one appointment type.
71
108
  *