@cosmoops/connect-sdk 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cjs/client/components/availability-form.js +52 -24
- package/dist/cjs/client/components/create-workspace-form.js +66 -0
- package/dist/cjs/client/components/event-type-form.js +99 -25
- package/dist/cjs/client/components/workspace-switcher.js +42 -9
- package/dist/cjs/client/index.js +11 -1
- package/dist/cjs/client/use-scheduling.js +37 -0
- package/dist/cjs/client/use-workspaces.js +41 -0
- package/dist/cjs/server/client.js +1 -1
- package/dist/cjs/server/handler.js +103 -2
- package/dist/cjs/server/resources/scheduling.js +67 -0
- package/dist/cjs/server/resources/workspaces.js +30 -11
- package/dist/esm/client/components/availability-form.js +53 -25
- package/dist/esm/client/components/create-workspace-form.js +63 -0
- package/dist/esm/client/components/event-type-form.js +100 -26
- package/dist/esm/client/components/workspace-switcher.js +42 -9
- package/dist/esm/client/index.js +6 -1
- package/dist/esm/client/use-scheduling.js +34 -0
- package/dist/esm/client/use-workspaces.js +38 -0
- package/dist/esm/server/client.js +1 -1
- package/dist/esm/server/handler.js +103 -2
- package/dist/esm/server/resources/scheduling.js +67 -0
- package/dist/esm/server/resources/workspaces.js +30 -11
- package/dist/types/client/components/availability-form.d.ts +23 -12
- package/dist/types/client/components/create-workspace-form.d.ts +32 -0
- package/dist/types/client/components/event-type-form.d.ts +30 -8
- package/dist/types/client/components/workspace-switcher.d.ts +9 -9
- package/dist/types/client/index.d.ts +5 -1
- package/dist/types/client/use-scheduling.d.ts +46 -2
- package/dist/types/client/use-workspaces.d.ts +27 -0
- package/dist/types/server/client.d.ts +1 -1
- package/dist/types/server/resources/scheduling.d.ts +66 -2
- package/dist/types/server/resources/workspaces.d.ts +28 -0
- package/dist/types/types.d.ts +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.useCreateWorkspace = useCreateWorkspace;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const provider_js_1 = require("./provider.js");
|
|
7
|
+
/**
|
|
8
|
+
* Opens another workspace under the tenant the signed-in user already owns.
|
|
9
|
+
*
|
|
10
|
+
* **Not optimistic**, same posture as every write in `use-scheduling.ts`:
|
|
11
|
+
* whether the tenant's plan still has room for another workspace is a
|
|
12
|
+
* question only the server can answer, and a modal that closes before that
|
|
13
|
+
* answer comes back is the lie a two-phase write exists to avoid.
|
|
14
|
+
*
|
|
15
|
+
* The result is a `UserWorkspace` row with `isActive`/`subscribed` both
|
|
16
|
+
* `false` - creating does not switch the session to it, and the workspace has
|
|
17
|
+
* no subscription yet. Pair with `<ConnectWorkspaceSwitcher>`'s own
|
|
18
|
+
* `mutateResource("/workspaces/switch", ...)` call to move into it, guarded
|
|
19
|
+
* on `inAudience` the same way that component's `selectable()` already is.
|
|
20
|
+
*/
|
|
21
|
+
function useCreateWorkspace() {
|
|
22
|
+
const { mutateResource } = (0, provider_js_1.useConnect)();
|
|
23
|
+
const [isPending, setPending] = (0, react_1.useState)(false);
|
|
24
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
25
|
+
const createWorkspace = (0, react_1.useCallback)(async (input) => {
|
|
26
|
+
setPending(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const result = await mutateResource("/workspaces", input);
|
|
30
|
+
return result.workspace;
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
setError(cause instanceof Error ? cause.message : "Could not create that workspace.");
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setPending(false);
|
|
38
|
+
}
|
|
39
|
+
}, [mutateResource]);
|
|
40
|
+
return { isPending, error, clearError: () => setError(null), createWorkspace };
|
|
41
|
+
}
|
|
@@ -741,8 +741,8 @@ function createConnectHandler(options) {
|
|
|
741
741
|
*/
|
|
742
742
|
"STORE": async (request) => storeRoute(request),
|
|
743
743
|
/**
|
|
744
|
-
* The workspace switcher's
|
|
745
|
-
* session token.
|
|
744
|
+
* The workspace switcher's reads and writes, forwarded with the caller's
|
|
745
|
+
* own session token.
|
|
746
746
|
*
|
|
747
747
|
* `POST /workspaces/switch` is the only bridge route that *changes the
|
|
748
748
|
* caller's session*, so it writes the new token back through the same
|
|
@@ -754,6 +754,14 @@ function createConnectHandler(options) {
|
|
|
754
754
|
* A partner with no `session.write` cannot re-persist a rotated token, so
|
|
755
755
|
* switching is simply absent for them rather than half-working - the same
|
|
756
756
|
* treatment `/signin` and `/callback` already get.
|
|
757
|
+
*
|
|
758
|
+
* `POST /workspaces` opens another workspace under the caller's own
|
|
759
|
+
* tenant and does not rotate anything - it does not switch the session,
|
|
760
|
+
* same as Connect's own "Create workspace" button. Every guard
|
|
761
|
+
* (ownership, the plan's workspace allowance, the business-email-domain
|
|
762
|
+
* rule) lives server-side in `CreateWorkspaceUseCase`; this route is a
|
|
763
|
+
* plain forward with the caller's session token, exactly like the reads
|
|
764
|
+
* beside it.
|
|
757
765
|
*/
|
|
758
766
|
/*
|
|
759
767
|
* Push notifications - the browser half, and deliberately only that half.
|
|
@@ -810,6 +818,20 @@ function createConnectHandler(options) {
|
|
|
810
818
|
return json(await client.push.revokeDevice(body.id, { asUser: token }));
|
|
811
819
|
},
|
|
812
820
|
"GET /workspaces": async (request) => json({ workspaces: await client.workspaces.list({ asUser: await requireToken(request) }) }),
|
|
821
|
+
"POST /workspaces": async (request) => {
|
|
822
|
+
const token = await requireToken(request);
|
|
823
|
+
const body = (await request.json().catch(() => null));
|
|
824
|
+
if (typeof body?.name !== "string" || (body.type !== "individual" && body.type !== "business")) {
|
|
825
|
+
throw new errors_js_1.ConnectError({
|
|
826
|
+
status: 400,
|
|
827
|
+
code: "validation_error",
|
|
828
|
+
message: "name and type are required",
|
|
829
|
+
endpoint: "POST /workspaces",
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const workspace = await client.workspaces.create({ name: body.name, type: body.type }, { asUser: token });
|
|
833
|
+
return json({ workspace });
|
|
834
|
+
},
|
|
813
835
|
"GET /workspaces/members": async (request) => json({ members: await client.workspaces.members({ asUser: await requireToken(request) }) }),
|
|
814
836
|
"POST /workspaces/switch": async (request) => {
|
|
815
837
|
const token = await requireToken(request);
|
|
@@ -893,6 +915,58 @@ function createConnectHandler(options) {
|
|
|
893
915
|
asUser,
|
|
894
916
|
}), 201);
|
|
895
917
|
},
|
|
918
|
+
/*
|
|
919
|
+
* The richer read `ConnectEventTypeFormModal`'s edit mode needs - see
|
|
920
|
+
* `listManageableEventTypes`'s own doc comment for why this is a separate
|
|
921
|
+
* route from the catalogue above rather than a wider response on it.
|
|
922
|
+
*/
|
|
923
|
+
"GET /scheduling/event-types/manage": async (request) => {
|
|
924
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
925
|
+
const asUser = await requireToken(request);
|
|
926
|
+
return json({
|
|
927
|
+
eventTypes: await client.scheduling.listManageableEventTypes(workspaceId, { asUser }),
|
|
928
|
+
});
|
|
929
|
+
},
|
|
930
|
+
/*
|
|
931
|
+
* A fixed path with the id in the body, not `/scheduling/event-types/{id}` -
|
|
932
|
+
* this table is matched by exact string, the same reason the booking
|
|
933
|
+
* actions below take their id this way rather than as a path segment.
|
|
934
|
+
*/
|
|
935
|
+
"POST /scheduling/event-types/update": async (request) => {
|
|
936
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
937
|
+
const asUser = await requireToken(request);
|
|
938
|
+
const body = (await request.json().catch(() => null));
|
|
939
|
+
if (typeof body?.eventTypeId !== "string") {
|
|
940
|
+
throw new errors_js_1.ConnectError({
|
|
941
|
+
status: 400,
|
|
942
|
+
code: "validation_error",
|
|
943
|
+
message: "eventTypeId is required",
|
|
944
|
+
endpoint: "/scheduling/event-types/update",
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return json(await client.scheduling.updateEventType({
|
|
948
|
+
workspaceId,
|
|
949
|
+
eventTypeId: body.eventTypeId,
|
|
950
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
951
|
+
description: typeof body.description === "string" || body.description === null
|
|
952
|
+
? body.description
|
|
953
|
+
: undefined,
|
|
954
|
+
durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : undefined,
|
|
955
|
+
bufferBeforeMinutes: typeof body.bufferBeforeMinutes === "number" ? body.bufferBeforeMinutes : undefined,
|
|
956
|
+
bufferAfterMinutes: typeof body.bufferAfterMinutes === "number" ? body.bufferAfterMinutes : undefined,
|
|
957
|
+
hostMemberId: typeof body.hostMemberId === "string" ? body.hostMemberId : undefined,
|
|
958
|
+
scheduleId: typeof body.scheduleId === "string" ? body.scheduleId : undefined,
|
|
959
|
+
location: body.location && typeof body.location.kind === "string"
|
|
960
|
+
? {
|
|
961
|
+
kind: body.location.kind,
|
|
962
|
+
value: typeof body.location.value === "string" ? body.location.value : null,
|
|
963
|
+
}
|
|
964
|
+
: undefined,
|
|
965
|
+
publicBookingEnabled: typeof body.publicBookingEnabled === "boolean" ? body.publicBookingEnabled : undefined,
|
|
966
|
+
active: typeof body.active === "boolean" ? body.active : undefined,
|
|
967
|
+
asUser,
|
|
968
|
+
}));
|
|
969
|
+
},
|
|
896
970
|
/*
|
|
897
971
|
* The working-hours templates an appointment type can point at - read
|
|
898
972
|
* before a setup UI offers to create one, so it can tell "none yet" from
|
|
@@ -931,6 +1005,33 @@ function createConnectHandler(options) {
|
|
|
931
1005
|
asUser,
|
|
932
1006
|
}), 201);
|
|
933
1007
|
},
|
|
1008
|
+
/*
|
|
1009
|
+
* A fixed path with the id in the body, same reasoning as the event
|
|
1010
|
+
* type's own `update` route above.
|
|
1011
|
+
*/
|
|
1012
|
+
"POST /scheduling/schedules/update": async (request) => {
|
|
1013
|
+
const { workspaceId } = await requireWorkspace(request);
|
|
1014
|
+
const asUser = await requireToken(request);
|
|
1015
|
+
const body = (await request.json().catch(() => null));
|
|
1016
|
+
if (typeof body?.scheduleId !== "string") {
|
|
1017
|
+
throw new errors_js_1.ConnectError({
|
|
1018
|
+
status: 400,
|
|
1019
|
+
code: "validation_error",
|
|
1020
|
+
message: "scheduleId is required",
|
|
1021
|
+
endpoint: "/scheduling/schedules/update",
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return json(await client.scheduling.updateSchedule({
|
|
1025
|
+
workspaceId,
|
|
1026
|
+
scheduleId: body.scheduleId,
|
|
1027
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
1028
|
+
timezone: typeof body.timezone === "string" ? body.timezone : undefined,
|
|
1029
|
+
weeklyHours: typeof body.weeklyHours === "object" && body.weeklyHours !== null
|
|
1030
|
+
? body.weeklyHours
|
|
1031
|
+
: undefined,
|
|
1032
|
+
asUser,
|
|
1033
|
+
}));
|
|
1034
|
+
},
|
|
934
1035
|
"GET /scheduling/slots": async (request) => {
|
|
935
1036
|
const { workspaceId } = await requireWorkspace(request);
|
|
936
1037
|
const asUser = await requireToken(request);
|
|
@@ -76,6 +76,9 @@ class SchedulingResource {
|
|
|
76
76
|
* Needs a schedule - create or pick one with `listSchedules`/`createSchedule`
|
|
77
77
|
* first. The slug for a future public booking link is derived from `name`
|
|
78
78
|
* server-side; you cannot choose one.
|
|
79
|
+
*
|
|
80
|
+
* Answers the richer *manageable* shape, not `BookableEventType` - you just
|
|
81
|
+
* created this row and are about to manage it, not book against it.
|
|
79
82
|
*/
|
|
80
83
|
createEventType(input) {
|
|
81
84
|
return (0, http_js_1.request)(this.transport, {
|
|
@@ -96,6 +99,70 @@ class SchedulingResource {
|
|
|
96
99
|
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
97
100
|
});
|
|
98
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The richer read `createEventType`/`updateEventType`'s edit mode needs -
|
|
104
|
+
* everything `listEventTypes`'s booking catalogue deliberately withholds,
|
|
105
|
+
* because this is the opposite audience: a member managing their own
|
|
106
|
+
* calendar, not a widget rendering someone else's. Includes inactive rows,
|
|
107
|
+
* since an edit screen is exactly where one gets reactivated.
|
|
108
|
+
*/
|
|
109
|
+
async listManageableEventTypes(workspaceId, options = {}) {
|
|
110
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
111
|
+
method: "GET",
|
|
112
|
+
path: "/scheduling/event-types/manage",
|
|
113
|
+
query: { workspaceId },
|
|
114
|
+
headers: (0, acting_as_js_1.actingAs)(options.asUser),
|
|
115
|
+
});
|
|
116
|
+
return result.eventTypes ?? [];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Edits an existing availability schedule.
|
|
120
|
+
*
|
|
121
|
+
* Narrower than what the workspace's own Calendar settings can do, the
|
|
122
|
+
* same way `createSchedule` is: `ownerMemberId` and date overrides stay
|
|
123
|
+
* untouched, since this surface never sets either.
|
|
124
|
+
*/
|
|
125
|
+
updateSchedule(input) {
|
|
126
|
+
return (0, http_js_1.request)(this.transport, {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
path: `/scheduling/schedules/${encodeURIComponent(input.scheduleId)}`,
|
|
129
|
+
json: {
|
|
130
|
+
workspaceId: input.workspaceId,
|
|
131
|
+
name: input.name,
|
|
132
|
+
timezone: input.timezone,
|
|
133
|
+
weeklyHours: input.weeklyHours,
|
|
134
|
+
},
|
|
135
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Edits an existing appointment type.
|
|
140
|
+
*
|
|
141
|
+
* `hostMemberId` omitted leaves the host untouched, including a *group*
|
|
142
|
+
* host this surface cannot represent - so an edit that says nothing about
|
|
143
|
+
* the host can never accidentally overwrite a team assignment made in
|
|
144
|
+
* Connect.
|
|
145
|
+
*/
|
|
146
|
+
updateEventType(input) {
|
|
147
|
+
return (0, http_js_1.request)(this.transport, {
|
|
148
|
+
method: "PUT",
|
|
149
|
+
path: `/scheduling/event-types/${encodeURIComponent(input.eventTypeId)}`,
|
|
150
|
+
json: {
|
|
151
|
+
workspaceId: input.workspaceId,
|
|
152
|
+
name: input.name,
|
|
153
|
+
description: input.description,
|
|
154
|
+
durationMinutes: input.durationMinutes,
|
|
155
|
+
bufferBeforeMinutes: input.bufferBeforeMinutes,
|
|
156
|
+
bufferAfterMinutes: input.bufferAfterMinutes,
|
|
157
|
+
hostMemberId: input.hostMemberId,
|
|
158
|
+
scheduleId: input.scheduleId,
|
|
159
|
+
location: input.location,
|
|
160
|
+
publicBookingEnabled: input.publicBookingEnabled,
|
|
161
|
+
active: input.active,
|
|
162
|
+
},
|
|
163
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
99
166
|
/**
|
|
100
167
|
* Free times for one appointment type over one window.
|
|
101
168
|
*
|
|
@@ -3,17 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WorkspacesResource = void 0;
|
|
4
4
|
const acting_as_js_1 = require("./acting-as.js");
|
|
5
5
|
const http_js_1 = require("../http.js");
|
|
6
|
-
/**
|
|
7
|
-
* Which workspaces the signed-in user belongs to, moving between them, who
|
|
8
|
-
* else is on the team, and inviting or granting access to your application.
|
|
9
|
-
*
|
|
10
|
-
* **Every call here needs `asUser`** - the signed-in user's opaque
|
|
11
|
-
* `sessionToken`. There is no application-wide form of any of these: they take
|
|
12
|
-
* no `userId` and no `workspaceId`, and resolve both from the token. That is
|
|
13
|
-
* what makes the whole module safe to reach from a browser through
|
|
14
|
-
* `createConnectHandler`, and it is why a partner cannot enumerate another
|
|
15
|
-
* tenant's team with their own API key.
|
|
16
|
-
*/
|
|
17
6
|
class WorkspacesResource {
|
|
18
7
|
transport;
|
|
19
8
|
constructor(transport) {
|
|
@@ -34,6 +23,36 @@ class WorkspacesResource {
|
|
|
34
23
|
});
|
|
35
24
|
return result.workspaces;
|
|
36
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Opens another workspace under the tenant the signed-in user already
|
|
28
|
+
* owns - a branch office, a second business unit.
|
|
29
|
+
*
|
|
30
|
+
* Every guard `CreateWorkspaceUseCase` enforces still applies: only the
|
|
31
|
+
* tenant's owner may call this, `type: "business"` still needs a non-free
|
|
32
|
+
* email domain, and the tenant's plan still decides whether it may hold
|
|
33
|
+
* another workspace at all - a `PreconditionError` surfaces as a thrown
|
|
34
|
+
* `ConnectError` with a message written to be shown ("Your current plan
|
|
35
|
+
* covers a single workspace...", "You've used all 3 of the workspaces
|
|
36
|
+
* your plans include..."). There is no `tenantId` parameter: the caller
|
|
37
|
+
* names no tenant, same as every other method on this resource names no
|
|
38
|
+
* workspace.
|
|
39
|
+
*
|
|
40
|
+
* `isActive`/`subscribed` on the result are always `false` - this does not
|
|
41
|
+
* switch the session, and the new workspace has no subscription yet. Chain
|
|
42
|
+
* `switch({workspaceId: result.workspaceId})` to move into it, but only
|
|
43
|
+
* when `result.inAudience` - a workspace outside this application's
|
|
44
|
+
* audience is refused by `switch` the same way `selectable()` in
|
|
45
|
+
* `<ConnectWorkspaceSwitcher>` already treats it.
|
|
46
|
+
*/
|
|
47
|
+
async create(input, options) {
|
|
48
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
path: "/workspaces",
|
|
51
|
+
headers: (0, acting_as_js_1.actingAs)(options.asUser),
|
|
52
|
+
json: input,
|
|
53
|
+
});
|
|
54
|
+
return result.workspace;
|
|
55
|
+
}
|
|
37
56
|
/**
|
|
38
57
|
* Moves this session to another workspace, returning a **new session
|
|
39
58
|
* token**. The one you passed is revoked.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Input, Label, Modal, Switch, TextField } from "@heroui/react";
|
|
5
|
-
import { useCreateAvailabilitySchedule } from "../use-scheduling.js";
|
|
5
|
+
import { useAvailabilitySchedules, useCreateAvailabilitySchedule, useUpdateAvailabilitySchedule, } from "../use-scheduling.js";
|
|
6
6
|
const WEEKDAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
7
7
|
const WEEKDAY_KEYS = [0, 1, 2, 3, 4, 5, 6];
|
|
8
8
|
/** Monday-Friday, 9am-5pm - the same starting point Connect's own Console form offers. */
|
|
@@ -20,24 +20,52 @@ function timeInputToMinutes(value) {
|
|
|
20
20
|
return (hours ?? 0) * 60 + (minutes ?? 0);
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
* rather than a link-out to Connect.
|
|
23
|
+
* Creates or edits a workspace's working hours, from inside a host
|
|
24
|
+
* application rather than a link-out to Connect.
|
|
25
25
|
*
|
|
26
26
|
* Deliberately narrower than Connect's own Calendar settings form: one
|
|
27
|
-
* interval per day (no split shifts), no date overrides, and the timezone
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* interval per day (no split shifts), no date overrides, and the timezone is
|
|
28
|
+
* set once at creation (from the browser's own) and shown but not editable
|
|
29
|
+
* here afterwards. This is a first-party setup-and-edit surface, not a
|
|
30
|
+
* second copy of the Console editor - "Manage in Connect" is still where the
|
|
31
|
+
* rest of what a schedule can do lives.
|
|
31
32
|
*
|
|
32
|
-
* **Not optimistic.**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* **Not optimistic.** The write settles against the server before this
|
|
34
|
+
* closes, the same posture every write in this package takes - see
|
|
35
|
+
* `ConnectBookingDialog`.
|
|
36
|
+
*
|
|
37
|
+
* This outer component only decides whether the row to edit has loaded yet;
|
|
38
|
+
* the form itself is `ScheduleFields` below, mounted with
|
|
39
|
+
* `key={editing?.id}` - see that component's own doc comment for why.
|
|
40
|
+
*/
|
|
41
|
+
export function ConnectAvailabilityFormModal({ isOpen, onOpenChange, scheduleId = null, onSaved, }) {
|
|
42
|
+
const isEdit = scheduleId !== null;
|
|
43
|
+
const { schedules, isLoading, error } = useAvailabilitySchedules();
|
|
44
|
+
const editing = isEdit ? schedules.find((row) => row.id === scheduleId) ?? null : null;
|
|
45
|
+
return (_jsx(Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { children: _jsxs(Modal.Dialog, { className: "sm:max-w-120", children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: isEdit ? "Edit availability" : "Set your availability" }) }), _jsx(Modal.Body, { children: isEdit && isLoading ? (_jsx("p", { className: "text-sm text-muted", children: "Loading\u2026" })) : isEdit && error ? (_jsx("p", { className: "text-sm text-muted", children: "That could not be loaded just now. Close this and try again." })) : isEdit && !editing ? (_jsx("p", { className: "text-sm text-muted", children: "Those working hours no longer exist." })) : (_jsx(ScheduleFields, { editing: editing, onCancel: () => onOpenChange(false), onSaved: (saved) => {
|
|
46
|
+
onSaved?.(saved);
|
|
47
|
+
onOpenChange(false);
|
|
48
|
+
} }, editing?.id ?? "create")) })] }) }) }) }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The form itself, mounted fresh (via the parent's `key`) for each distinct
|
|
52
|
+
* record it edits - or once for create. That remount is what initializes
|
|
53
|
+
* every field straight from `editing` with a plain `useState(initial)`
|
|
54
|
+
* rather than a `useEffect` copying it in after the fact: `editing` arrives
|
|
55
|
+
* asynchronously (`useAvailabilitySchedules` is a bridge read), and the
|
|
56
|
+
* parent already withholds this component until it has landed, so there is
|
|
57
|
+
* no later moment for a field to overwrite a half-typed edit.
|
|
35
58
|
*/
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const [
|
|
39
|
-
const [
|
|
40
|
-
const
|
|
59
|
+
function ScheduleFields({ editing, onCancel, onSaved, }) {
|
|
60
|
+
const isEdit = editing !== null;
|
|
61
|
+
const [name, setName] = useState(editing?.name ?? "Working hours");
|
|
62
|
+
const [timezone] = useState(() => editing?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC");
|
|
63
|
+
const [weeklyHours, setWeeklyHours] = useState(editing?.weeklyHours ?? defaultWeeklyHours);
|
|
64
|
+
const { createSchedule, isPending: isCreating, error: createError, clearError: clearCreateError } = useCreateAvailabilitySchedule();
|
|
65
|
+
const { updateSchedule, isPending: isUpdating, error: updateError, clearError: clearUpdateError } = useUpdateAvailabilitySchedule();
|
|
66
|
+
const isPending = isCreating || isUpdating;
|
|
67
|
+
const error = isEdit ? updateError : createError;
|
|
68
|
+
const clearError = isEdit ? clearUpdateError : clearCreateError;
|
|
41
69
|
const toggleDay = (day, open) => {
|
|
42
70
|
setWeeklyHours((prev) => ({
|
|
43
71
|
...prev,
|
|
@@ -58,15 +86,15 @@ export function ConnectAvailabilityFormModal({ isOpen, onOpenChange, onCreated,
|
|
|
58
86
|
if (!isFormValid)
|
|
59
87
|
return;
|
|
60
88
|
clearError();
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
const saved = isEdit
|
|
90
|
+
? await updateSchedule({ scheduleId: editing.id, name: name.trim(), weeklyHours })
|
|
91
|
+
: await createSchedule({ name: name.trim(), timezone, weeklyHours });
|
|
92
|
+
if (saved)
|
|
93
|
+
onSaved(saved);
|
|
66
94
|
};
|
|
67
|
-
return (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs(TextField, { isRequired: true, variant: "secondary", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), _jsxs("p", { className: "text-xs text-muted", children: ["Timezone: ", timezone] }), _jsx("div", { className: "flex flex-col gap-2", children: WEEKDAY_KEYS.map((day) => {
|
|
96
|
+
const interval = weeklyHours[day][0];
|
|
97
|
+
const isOpenDay = interval !== undefined;
|
|
98
|
+
return (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { isSelected: isOpenDay, onChange: (open) => toggleDay(day, open), isDisabled: isPending, children: _jsxs(Switch.Content, { children: [_jsx(Switch.Control, { children: _jsx(Switch.Thumb, {}) }), _jsx("span", { className: "w-24 shrink-0 text-sm", children: WEEKDAY_LABELS[day] })] }) }), isOpenDay && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "time", value: minutesToTimeInput(interval.startMinute), onChange: (e) => setDayTime(day, "startMinute", e.currentTarget.value), disabled: isPending }), _jsx("span", { className: "text-xs text-muted", children: "to" }), _jsx(Input, { type: "time", value: minutesToTimeInput(interval.endMinute), onChange: (e) => setDayTime(day, "endMinute", e.currentTarget.value), disabled: isPending })] }))] }, day));
|
|
99
|
+
}) }), error && _jsx("p", { className: "text-sm text-danger", children: error }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Saving..." : isEdit ? "Save changes" : "Save" })] })] }));
|
|
72
100
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button, Input, Label, Modal, Radio, RadioGroup, TextField } from "@heroui/react";
|
|
5
|
+
import { useCreateWorkspace } from "../use-workspaces.js";
|
|
6
|
+
const WORKSPACE_TYPES = [
|
|
7
|
+
{
|
|
8
|
+
id: "business",
|
|
9
|
+
label: "Business",
|
|
10
|
+
hint: "A company. Verified with company KYB, and can subscribe to anything sold to businesses.",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "individual",
|
|
14
|
+
label: "Individual",
|
|
15
|
+
hint: "A person. Verified against your own identity rather than a company.",
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Opens another workspace under the signed-in user's own tenant - a branch
|
|
20
|
+
* office, a second business unit - from inside a host application rather
|
|
21
|
+
* than a link-out to Connect.
|
|
22
|
+
*
|
|
23
|
+
* **Only the tenant's owner can use this successfully.** `CreateWorkspaceUseCase`
|
|
24
|
+
* refuses anyone who merely belongs to a colleague's tenant, with a message
|
|
25
|
+
* naming that - someone who wants a workspace of their own uses a personal
|
|
26
|
+
* account instead.
|
|
27
|
+
*
|
|
28
|
+
* **The tenant's plan decides how many workspaces it may hold, and this does
|
|
29
|
+
* not pre-check it.** There is no capacity read to show ahead of time - the
|
|
30
|
+
* same posture Connect's own "Create workspace" button takes - so a refusal
|
|
31
|
+
* surfaces here exactly as the server wrote it ("Your current plan covers a
|
|
32
|
+
* single workspace - subscribe to a plan that includes additional workspaces
|
|
33
|
+
* to open another.", or "You've used all 3 of the workspaces your plans
|
|
34
|
+
* include. Upgrade a plan or delete a workspace to open another.").
|
|
35
|
+
*
|
|
36
|
+
* **Does not switch the session to the new workspace.** Creating one never
|
|
37
|
+
* does, on Connect's own screen either - see `useCreateWorkspace`'s doc
|
|
38
|
+
* comment for how to chain a switch once you have the result, and guard that
|
|
39
|
+
* chain on `inAudience` the same way `<ConnectWorkspaceSwitcher>` already
|
|
40
|
+
* does for every other row.
|
|
41
|
+
*/
|
|
42
|
+
export function ConnectCreateWorkspaceModal({ isOpen, onOpenChange, onCreated, }) {
|
|
43
|
+
return (_jsx(Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { children: _jsxs(Modal.Dialog, { className: "sm:max-w-110", children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Create workspace" }) }), _jsx(Modal.Body, { children: _jsx(CreateWorkspaceFields, { onCancel: () => onOpenChange(false), onCreated: (workspace) => {
|
|
44
|
+
onCreated?.(workspace);
|
|
45
|
+
onOpenChange(false);
|
|
46
|
+
} }) })] }) }) }) }));
|
|
47
|
+
}
|
|
48
|
+
function CreateWorkspaceFields({ onCancel, onCreated, }) {
|
|
49
|
+
const [name, setName] = useState("");
|
|
50
|
+
const [type, setType] = useState("business");
|
|
51
|
+
const { createWorkspace, isPending, error, clearError } = useCreateWorkspace();
|
|
52
|
+
const isFormValid = name.trim().length > 0 && !isPending;
|
|
53
|
+
const handleSubmit = async (e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
if (!isFormValid)
|
|
56
|
+
return;
|
|
57
|
+
clearError();
|
|
58
|
+
const created = await createWorkspace({ name: name.trim(), type });
|
|
59
|
+
if (created)
|
|
60
|
+
onCreated(created);
|
|
61
|
+
};
|
|
62
|
+
return (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs(TextField, { isRequired: true, variant: "secondary", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { placeholder: "Acme Logistics", value: name, onChange: (e) => setName(e.currentTarget.value), disabled: isPending, autoFocus: true })] }), _jsxs(RadioGroup, { variant: "secondary", value: type, onChange: (value) => setType(value), isDisabled: isPending, children: [_jsx(Label, { children: "Type" }), _jsx("div", { className: "mt-1.5 flex flex-col gap-2", children: WORKSPACE_TYPES.map((option) => (_jsx(Radio, { value: option.id, children: _jsxs(Radio.Content, { children: [_jsx(Radio.Control, { children: _jsx(Radio.Indicator, {}) }), _jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: option.label }), _jsx("p", { className: "text-xs text-muted", children: option.hint })] })] }) }, option.id))) })] }), error && _jsx("p", { className: "text-sm text-danger", children: error }), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", onPress: onCancel, isDisabled: isPending, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", isDisabled: !isFormValid, children: isPending ? "Creating…" : "Create" })] })] }));
|
|
63
|
+
}
|