@cosmoops/connect-sdk 0.38.0 → 0.41.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 +86 -0
- package/dist/cjs/client/components/availability-form.js +52 -24
- package/dist/cjs/client/components/call-button.js +398 -0
- 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 +17 -2
- package/dist/cjs/client/portal-links.js +1 -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 +176 -2
- package/dist/cjs/server/resources/calls.js +58 -0
- 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/call-button.js +358 -0
- 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 +8 -2
- package/dist/esm/client/portal-links.js +1 -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 +176 -2
- package/dist/esm/server/resources/calls.js +58 -0
- 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/call-button.d.ts +132 -0
- 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 +7 -1
- package/dist/types/client/portal-links.d.ts +1 -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/calls.d.ts +32 -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 +143 -1
- package/package.json +4 -1
|
@@ -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);
|
|
@@ -1112,6 +1213,67 @@ function createConnectHandler(options) {
|
|
|
1112
1213
|
const items = await client.sessions.list(await requireToken(request));
|
|
1113
1214
|
return json({ items });
|
|
1114
1215
|
},
|
|
1216
|
+
/*
|
|
1217
|
+
* Browser calling - `<ConnectCallButton>`. Three fixed writes and no read:
|
|
1218
|
+
* how a call went is your *server's* question (`client.calls.get`), never
|
|
1219
|
+
* the browser's, so a call log can't be written from what a page claims.
|
|
1220
|
+
*
|
|
1221
|
+
* The workspace and the member both come from the session, as everywhere
|
|
1222
|
+
* here; the browser names only the number.
|
|
1223
|
+
*/
|
|
1224
|
+
/*
|
|
1225
|
+
* The one calling *read*, and it is about the member, not about a call:
|
|
1226
|
+
* can they ring anybody from here. A refusal means they lack "Make calls"
|
|
1227
|
+
* - which is an answer, not a failure, so it is returned as one and the
|
|
1228
|
+
* button simply is not offered.
|
|
1229
|
+
*/
|
|
1230
|
+
"GET /calls/availability": async (request) => {
|
|
1231
|
+
await requireWorkspace(request);
|
|
1232
|
+
const asUser = await requireToken(request);
|
|
1233
|
+
try {
|
|
1234
|
+
return json(await client.calls.getCallingAvailability({ asUser }));
|
|
1235
|
+
}
|
|
1236
|
+
catch (error) {
|
|
1237
|
+
if (error instanceof errors_js_1.ConnectError && error.status === 403) {
|
|
1238
|
+
return json(NO_CALLING_PERMISSION);
|
|
1239
|
+
}
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
"POST /calls/browser-token": async (request) => {
|
|
1244
|
+
await requireWorkspace(request);
|
|
1245
|
+
const asUser = await requireToken(request);
|
|
1246
|
+
return json(await client.calls.getBrowserCallingToken({ asUser }));
|
|
1247
|
+
},
|
|
1248
|
+
"POST /calls/start": async (request) => {
|
|
1249
|
+
await requireWorkspace(request);
|
|
1250
|
+
const asUser = await requireToken(request);
|
|
1251
|
+
const body = (await request.json().catch(() => null));
|
|
1252
|
+
if (typeof body?.phone !== "string") {
|
|
1253
|
+
throw new errors_js_1.ConnectError({
|
|
1254
|
+
status: 400,
|
|
1255
|
+
code: "validation_error",
|
|
1256
|
+
message: "phone is required",
|
|
1257
|
+
endpoint: "/calls/start",
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
return json(await client.calls.startBrowserCall({ phone: body.phone, asUser }), 201);
|
|
1261
|
+
},
|
|
1262
|
+
"POST /calls/cancel": async (request) => {
|
|
1263
|
+
await requireWorkspace(request);
|
|
1264
|
+
const asUser = await requireToken(request);
|
|
1265
|
+
const body = (await request.json().catch(() => null));
|
|
1266
|
+
if (typeof body?.callId !== "string") {
|
|
1267
|
+
throw new errors_js_1.ConnectError({
|
|
1268
|
+
status: 400,
|
|
1269
|
+
code: "validation_error",
|
|
1270
|
+
message: "callId is required",
|
|
1271
|
+
endpoint: "/calls/cancel",
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
await client.calls.cancelBrowserCall({ callId: body.callId, asUser });
|
|
1275
|
+
return json({ cancelled: true });
|
|
1276
|
+
},
|
|
1115
1277
|
"POST /sessions/revoke": async (request) => {
|
|
1116
1278
|
const body = (await request.json().catch(() => null));
|
|
1117
1279
|
if (!body?.sessionId) {
|
|
@@ -1397,6 +1559,18 @@ function createConnectHandler(options) {
|
|
|
1397
1559
|
}
|
|
1398
1560
|
};
|
|
1399
1561
|
}
|
|
1562
|
+
/** What a member without "Make calls" is told - see `GET /calls/availability`. */
|
|
1563
|
+
const NO_CALLING_PERMISSION = {
|
|
1564
|
+
status: "no_permission",
|
|
1565
|
+
number: null,
|
|
1566
|
+
available: 0,
|
|
1567
|
+
availableMinutes: 0,
|
|
1568
|
+
ratePaisePerMinute: 0,
|
|
1569
|
+
lowBalance: false,
|
|
1570
|
+
canTopUp: false,
|
|
1571
|
+
canManageNumber: false,
|
|
1572
|
+
offer: null,
|
|
1573
|
+
};
|
|
1400
1574
|
function json(body, status = 200) {
|
|
1401
1575
|
return new Response(JSON.stringify(body), {
|
|
1402
1576
|
status,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CallsResource = void 0;
|
|
4
4
|
const http_js_1 = require("../http.js");
|
|
5
|
+
const acting_as_js_1 = require("./acting-as.js");
|
|
5
6
|
/**
|
|
6
7
|
* Every call that arrived on a channel assigned to your application.
|
|
7
8
|
*
|
|
@@ -53,5 +54,62 @@ class CallsResource {
|
|
|
53
54
|
path: `/calls/${encodeURIComponent(callId)}`,
|
|
54
55
|
});
|
|
55
56
|
}
|
|
57
|
+
/*
|
|
58
|
+
* Browser calling - what `<ConnectCallButton>` runs on, through your bridge.
|
|
59
|
+
*
|
|
60
|
+
* All three need the `calls:place` scope **and** a signed-in member
|
|
61
|
+
* (`asUser`), whose own "Make calls" permission Connect checks. None works
|
|
62
|
+
* server-to-server: a call is placed by a person, from the workspace's own
|
|
63
|
+
* number, and is not recorded. Read how one went with `get(callId)`, which
|
|
64
|
+
* needs `calls:read` - a call your application placed is readable by it.
|
|
65
|
+
*/
|
|
66
|
+
/**
|
|
67
|
+
* Whether the member can call, and if not why - what `<ConnectCallButton>`
|
|
68
|
+
* renders from. Throws `ConnectError` 403 for a member without "Make
|
|
69
|
+
* calls"; the bridge turns that into `status: "no_permission"`.
|
|
70
|
+
*/
|
|
71
|
+
getCallingAvailability(input) {
|
|
72
|
+
return (0, http_js_1.request)(this.transport, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
path: "/calls/availability",
|
|
75
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** A carrier token the member's browser signs in to the phone network with. */
|
|
79
|
+
getBrowserCallingToken(input) {
|
|
80
|
+
return (0, http_js_1.request)(this.transport, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/calls/browser-token",
|
|
83
|
+
json: {},
|
|
84
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Approves one call from the member to `phone` (E.164) and records it.
|
|
89
|
+
*
|
|
90
|
+
* Throws `ConnectError` with status `412` when the workspace has no active
|
|
91
|
+
* number, is not allowed to place calls, or the member is already on one -
|
|
92
|
+
* the message is written to be shown to them.
|
|
93
|
+
*/
|
|
94
|
+
startBrowserCall(input) {
|
|
95
|
+
return (0, http_js_1.request)(this.transport, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
path: "/calls/start",
|
|
98
|
+
json: { phone: input.phone },
|
|
99
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Closes a call the browser never managed to place - recorded as
|
|
104
|
+
* `unanswered`/`cancelled`. A no-op once the carrier has it.
|
|
105
|
+
*/
|
|
106
|
+
async cancelBrowserCall(input) {
|
|
107
|
+
await (0, http_js_1.request)(this.transport, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
path: "/calls/cancel",
|
|
110
|
+
json: { callId: input.callId },
|
|
111
|
+
headers: (0, acting_as_js_1.actingAs)(input.asUser),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
56
114
|
}
|
|
57
115
|
exports.CallsResource = CallsResource;
|
|
@@ -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
|
}
|