@cosmoops/connect-sdk 0.37.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 (47) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs/client/components/availability-form.js +103 -0
  3. package/dist/cjs/client/components/create-workspace-form.js +66 -0
  4. package/dist/cjs/client/components/event-type-form.js +156 -0
  5. package/dist/cjs/client/components/user-menu.js +3 -24
  6. package/dist/cjs/client/components/workspace-switcher.js +42 -9
  7. package/dist/cjs/client/index.js +31 -3
  8. package/dist/cjs/client/portal-links.js +38 -0
  9. package/dist/cjs/client/use-scheduling.js +104 -8
  10. package/dist/cjs/client/use-workspaces.js +41 -0
  11. package/dist/cjs/server/client.js +1 -1
  12. package/dist/cjs/server/handler.js +181 -2
  13. package/dist/cjs/server/http.js +13 -1
  14. package/dist/cjs/server/resources/ai.js +6 -0
  15. package/dist/cjs/server/resources/scheduling.js +129 -0
  16. package/dist/cjs/server/resources/visitor.js +6 -0
  17. package/dist/cjs/server/resources/workspaces.js +30 -11
  18. package/dist/esm/client/components/availability-form.js +100 -0
  19. package/dist/esm/client/components/create-workspace-form.js +63 -0
  20. package/dist/esm/client/components/event-type-form.js +153 -0
  21. package/dist/esm/client/components/user-menu.js +3 -24
  22. package/dist/esm/client/components/workspace-switcher.js +42 -9
  23. package/dist/esm/client/index.js +17 -1
  24. package/dist/esm/client/portal-links.js +34 -0
  25. package/dist/esm/client/use-scheduling.js +98 -8
  26. package/dist/esm/client/use-workspaces.js +38 -0
  27. package/dist/esm/server/client.js +1 -1
  28. package/dist/esm/server/handler.js +181 -2
  29. package/dist/esm/server/http.js +12 -1
  30. package/dist/esm/server/resources/ai.js +7 -1
  31. package/dist/esm/server/resources/scheduling.js +129 -0
  32. package/dist/esm/server/resources/visitor.js +7 -1
  33. package/dist/esm/server/resources/workspaces.js +30 -11
  34. package/dist/types/client/components/availability-form.d.ts +33 -0
  35. package/dist/types/client/components/create-workspace-form.d.ts +32 -0
  36. package/dist/types/client/components/event-type-form.d.ts +44 -0
  37. package/dist/types/client/components/workspace-switcher.d.ts +9 -9
  38. package/dist/types/client/index.d.ts +10 -1
  39. package/dist/types/client/portal-links.d.ts +22 -0
  40. package/dist/types/client/use-scheduling.d.ts +93 -1
  41. package/dist/types/client/use-workspaces.d.ts +27 -0
  42. package/dist/types/server/client.d.ts +1 -1
  43. package/dist/types/server/http.d.ts +35 -0
  44. package/dist/types/server/resources/scheduling.d.ts +131 -1
  45. package/dist/types/server/resources/workspaces.d.ts +28 -0
  46. package/dist/types/types.d.ts +60 -0
  47. 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
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectAvailabilityFormModal = ConnectAvailabilityFormModal;
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_scheduling_js_1 = require("../use-scheduling.js");
9
+ const WEEKDAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
10
+ const WEEKDAY_KEYS = [0, 1, 2, 3, 4, 5, 6];
11
+ /** Monday-Friday, 9am-5pm - the same starting point Connect's own Console form offers. */
12
+ function defaultWeeklyHours() {
13
+ const open = [{ startMinute: 9 * 60, endMinute: 17 * 60 }];
14
+ return { 0: [], 1: open, 2: open, 3: open, 4: open, 5: open, 6: [] };
15
+ }
16
+ function minutesToTimeInput(minutes) {
17
+ const hours = Math.floor(minutes / 60);
18
+ const mins = minutes % 60;
19
+ return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
20
+ }
21
+ function timeInputToMinutes(value) {
22
+ const [hours, minutes] = value.split(":").map(Number);
23
+ return (hours ?? 0) * 60 + (minutes ?? 0);
24
+ }
25
+ /**
26
+ * Creates or edits a workspace's working hours, from inside a host
27
+ * application rather than a link-out to Connect.
28
+ *
29
+ * Deliberately narrower than Connect's own Calendar settings form: one
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.
35
+ *
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.
61
+ */
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;
72
+ const toggleDay = (day, open) => {
73
+ setWeeklyHours((prev) => ({
74
+ ...prev,
75
+ [day]: open ? [{ startMinute: 9 * 60, endMinute: 17 * 60 }] : [],
76
+ }));
77
+ };
78
+ const setDayTime = (day, edge, value) => {
79
+ setWeeklyHours((prev) => {
80
+ const interval = prev[day][0];
81
+ if (!interval)
82
+ return prev;
83
+ return { ...prev, [day]: [{ ...interval, [edge]: timeInputToMinutes(value) }] };
84
+ });
85
+ };
86
+ const isFormValid = name.trim().length > 0 && !isPending;
87
+ const handleSubmit = async (e) => {
88
+ e.preventDefault();
89
+ if (!isFormValid)
90
+ return;
91
+ clearError();
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);
97
+ };
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" })] })] }));
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
+ }
@@ -0,0 +1,156 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectEventTypeFormModal = ConnectEventTypeFormModal;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const provider_js_1 = require("../provider.js");
9
+ const use_scheduling_js_1 = require("../use-scheduling.js");
10
+ const LOCATION_LABELS = {
11
+ phone: "Phone call",
12
+ address: "In person",
13
+ link: "Video link",
14
+ };
15
+ const LOCATION_KINDS = ["phone", "address", "link"];
16
+ /**
17
+ * Creates or edits an appointment type, from inside a host application
18
+ * rather than a link-out to Connect.
19
+ *
20
+ * **Needs a schedule to exist first** - the picker reads `useAvailabilitySchedules`
21
+ * and renders "Set up availability first" with nothing to submit if none
22
+ * exists yet. Pair this with `ConnectAvailabilityFormModal` in a setup flow
23
+ * rather than assuming one.
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
+ *
34
+ * **Host is a specific member, never a team.** There is no groups read in
35
+ * this bridge, so a round-robin team host stays a Console-only capability -
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.
44
+ */
45
+ function ConnectEventTypeFormModal({ isOpen, onOpenChange, eventTypeId = null, onSaved, }) {
46
+ const isEdit = eventTypeId !== null;
47
+ const { data: membersData } = (0, provider_js_1.useConnectWorkspaceMembers)();
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)();
104
+ const defaultHostId = (0, react_1.useMemo)(() => {
105
+ const self = members.find((member) => member.userId === session?.user?.userId);
106
+ return self?.memberId ?? members[0]?.memberId ?? "";
107
+ }, [members, session?.user?.userId]);
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 ?? "");
117
+ const effectiveScheduleId = scheduleId || schedules[0]?.id || "";
118
+ const effectiveHostMemberId = hostMemberId || defaultHostId;
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;
124
+ const isFormValid = name.trim().length > 0 &&
125
+ durationMinutes >= 5 &&
126
+ durationMinutes <= 480 &&
127
+ effectiveScheduleId.length > 0 &&
128
+ effectiveHostMemberId.length > 0 &&
129
+ !isPending;
130
+ const handleSubmit = async (e) => {
131
+ e.preventDefault();
132
+ if (!isFormValid)
133
+ return;
134
+ clearError();
135
+ const trimmedDescription = description.trim();
136
+ const shared = {
137
+ name: name.trim(),
138
+ description: trimmedDescription.length > 0 ? trimmedDescription : null,
139
+ durationMinutes,
140
+ bufferBeforeMinutes: bufferBefore,
141
+ bufferAfterMinutes: bufferAfter,
142
+ hostMemberId: effectiveHostMemberId,
143
+ scheduleId: effectiveScheduleId,
144
+ location: {
145
+ kind: locationKind,
146
+ value: locationValue.trim().length > 0 ? locationValue.trim() : null,
147
+ },
148
+ };
149
+ const saved = isEdit
150
+ ? await updateEventType({ eventTypeId: editing.id, ...shared })
151
+ : await createEventType(shared);
152
+ if (saved)
153
+ onSaved(saved);
154
+ };
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" })] })] }));
156
+ }
@@ -13,6 +13,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
13
13
  const react_1 = require("@heroui/react");
14
14
  const lucide_react_1 = require("lucide-react");
15
15
  const provider_js_1 = require("../provider.js");
16
+ const portal_links_js_1 = require("../portal-links.js");
16
17
  const mark_js_1 = require("../mark.js");
17
18
  const identity_js_1 = require("./identity.js");
18
19
  const account_sessions_js_1 = require("./account-sessions.js");
@@ -28,28 +29,6 @@ const THEME_OPTIONS = [
28
29
  function openExternal(url) {
29
30
  window.open(url, "_blank", "noopener,noreferrer");
30
31
  }
31
- /**
32
- * Connect's profile page, with the context it needs to offer a way back here.
33
- *
34
- * Connect answers `/profile` for any signed-in user, so the bare URL works on
35
- * its own - these two params only add the "Back to <your app>" banner. It
36
- * validates `return_to` against this `client_id`'s registered redirect
37
- * origins and silently drops the banner if they don't match, so a wrong or
38
- * stale `appUrl`/`clientId` pairing degrades to what it did before rather
39
- * than erroring.
40
- *
41
- * `window.location.href` rather than a configured home URL: the point is to
42
- * come back to the page they left, and only the browser knows which one that
43
- * is.
44
- */
45
- function profileUrl(appUrl, clientId) {
46
- const url = new URL("/profile", appUrl);
47
- url.searchParams.set("client_id", clientId);
48
- if (typeof window !== "undefined") {
49
- url.searchParams.set("return_to", window.location.href);
50
- }
51
- return url.toString();
52
- }
53
32
  /**
54
33
  * The member's photo, with their initials underneath it.
55
34
  *
@@ -150,7 +129,7 @@ function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
150
129
  const label = user.displayName ?? user.email ?? "Account";
151
130
  const role = session?.role ?? null;
152
131
  const planChip = subscriptionChip(session?.subscription ?? null);
153
- return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: profileUrl(appUrl, clientId) }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
132
+ return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: (0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile") }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
154
133
  const [next] = [...keys];
155
134
  if (typeof next === "string")
156
135
  onThemeChange(next);
@@ -260,7 +239,7 @@ function ConnectProfileButton({ className }) {
260
239
  const { session, appUrl, clientId } = (0, provider_js_1.useConnect)();
261
240
  if (!session?.user)
262
241
  return null;
263
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(profileUrl(appUrl, clientId)), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
242
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal((0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile")), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
264
243
  }
265
244
  /** A plain "Open Connect" button, for a footer or a help menu. */
266
245
  function ConnectHomeButton({ className }) {
@@ -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) {