@pylonsync/client 0.3.268 → 0.3.270
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/package.json +3 -3
- package/src/components/OrganizationSwitcher.tsx +15 -2
- package/src/index.ts +2 -0
- package/src/lib/api.ts +48 -16
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.3.
|
|
6
|
+
"version": "0.3.270",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "src/index.ts",
|
|
9
9
|
"types": "src/index.ts",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"check": "tsc -p tsconfig.json --noEmit"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@pylonsync/react": "0.3.
|
|
23
|
-
"@pylonsync/sync": "0.3.
|
|
22
|
+
"@pylonsync/react": "0.3.270",
|
|
23
|
+
"@pylonsync/sync": "0.3.270",
|
|
24
24
|
"clsx": "^2.1.1"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
@@ -27,6 +27,14 @@ export interface OrganizationSwitcherProps {
|
|
|
27
27
|
onSwitched?: (orgId: string | null) => void;
|
|
28
28
|
/** Called after a new org is created (post-creation, pre-switch). */
|
|
29
29
|
onCreated?: (org: OrgSummary) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Active org name resolved on the server (e.g. `serverData.get("Org", …)`
|
|
32
|
+
* in an SSR layout). Seeds the trigger label so it renders the real name on
|
|
33
|
+
* the first paint — no "Personal" → real-name flash, and no pop-in waiting
|
|
34
|
+
* for `useAuth()`/`listOrgs()` to hydrate. The dropdown list still loads
|
|
35
|
+
* client-side, but it's hidden until opened.
|
|
36
|
+
*/
|
|
37
|
+
initialActiveName?: string;
|
|
30
38
|
className?: string;
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -44,6 +52,7 @@ export function OrganizationSwitcher({
|
|
|
44
52
|
hideCreate,
|
|
45
53
|
onSwitched,
|
|
46
54
|
onCreated,
|
|
55
|
+
initialActiveName,
|
|
47
56
|
className,
|
|
48
57
|
}: OrganizationSwitcherProps) {
|
|
49
58
|
const { isSignedIn, tenantId, selectOrg, clearOrg } = useAuth();
|
|
@@ -88,10 +97,14 @@ export function OrganizationSwitcher({
|
|
|
88
97
|
};
|
|
89
98
|
}, [open]);
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
// A server-seeded active name means the SSR layout already rendered this in
|
|
101
|
+
// an authenticated shell — render immediately instead of waiting for
|
|
102
|
+
// `isSignedIn` to hydrate (which causes a pop-in).
|
|
103
|
+
if (!isSignedIn && !initialActiveName) return null;
|
|
92
104
|
|
|
93
105
|
const active = orgs?.find((o) => o.id === tenantId) ?? null;
|
|
94
|
-
const label =
|
|
106
|
+
const label =
|
|
107
|
+
active?.name ?? initialActiveName ?? (hidePersonal ? "Select org" : "Personal");
|
|
95
108
|
|
|
96
109
|
async function switchTo(orgId: string | null) {
|
|
97
110
|
setSwitching(true);
|
package/src/index.ts
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -30,6 +30,29 @@ class ApiError extends Error {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Turn a non-2xx Response into an ApiError, pulling the stable error `code` and
|
|
34
|
+
// human `message` out of Pylon's error envelope. The server returns
|
|
35
|
+
// `{ "error": { "code": "...", "message": "..." } }`; older/other endpoints may
|
|
36
|
+
// send a flat `{ "error": "CODE", "message": "..." }`. Handle both so callers
|
|
37
|
+
// always get a real `.code` to branch on (not "Bad Request").
|
|
38
|
+
async function throwApiError(res: Response): Promise<never> {
|
|
39
|
+
const payload = (await res.json().catch(() => ({}))) as {
|
|
40
|
+
error?: { code?: string; message?: string } | string;
|
|
41
|
+
code?: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
};
|
|
44
|
+
const nested =
|
|
45
|
+
payload && typeof payload.error === "object" ? payload.error : null;
|
|
46
|
+
const code =
|
|
47
|
+
nested?.code ??
|
|
48
|
+
(typeof payload?.error === "string" ? payload.error : undefined) ??
|
|
49
|
+
payload?.code ??
|
|
50
|
+
`HTTP_${res.status}`;
|
|
51
|
+
const message =
|
|
52
|
+
nested?.message ?? payload?.message ?? res.statusText ?? "request failed";
|
|
53
|
+
throw new ApiError(code, message, res.status);
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
async function post<T>(path: string, body: unknown): Promise<T> {
|
|
34
57
|
const res = await fetch(`${getBaseUrl()}${path}`, {
|
|
35
58
|
method: "POST",
|
|
@@ -37,13 +60,7 @@ async function post<T>(path: string, body: unknown): Promise<T> {
|
|
|
37
60
|
headers: { "content-type": "application/json" },
|
|
38
61
|
body: JSON.stringify(body),
|
|
39
62
|
});
|
|
40
|
-
if (!res.ok)
|
|
41
|
-
const payload = await res.json().catch(() => ({}) as Record<string, unknown>);
|
|
42
|
-
const code = (payload?.error as string) ?? `HTTP_${res.status}`;
|
|
43
|
-
const message =
|
|
44
|
-
(payload?.message as string) ?? res.statusText ?? "request failed";
|
|
45
|
-
throw new ApiError(code, message, res.status);
|
|
46
|
-
}
|
|
63
|
+
if (!res.ok) await throwApiError(res);
|
|
47
64
|
return res.json() as Promise<T>;
|
|
48
65
|
}
|
|
49
66
|
|
|
@@ -52,7 +69,7 @@ async function get<T>(path: string): Promise<T> {
|
|
|
52
69
|
method: "GET",
|
|
53
70
|
credentials: "include",
|
|
54
71
|
});
|
|
55
|
-
if (!res.ok)
|
|
72
|
+
if (!res.ok) await throwApiError(res);
|
|
56
73
|
return res.json() as Promise<T>;
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -154,10 +171,31 @@ export async function createOrg(name: string): Promise<OrgSummary> {
|
|
|
154
171
|
return post<OrgSummary>("/api/auth/orgs", { name });
|
|
155
172
|
}
|
|
156
173
|
|
|
174
|
+
/** Rename an org. Server gates this to owners + admins (`can_manage_members`). */
|
|
175
|
+
export async function renameOrg(
|
|
176
|
+
orgId: string,
|
|
177
|
+
name: string,
|
|
178
|
+
): Promise<{ id: string; name: string }> {
|
|
179
|
+
return req<{ id: string; name: string }>("PATCH", `/api/auth/orgs/${orgId}`, {
|
|
180
|
+
name,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Delete an org and its memberships + invites. Server gates this to owners
|
|
185
|
+
* (`can_delete_org`). Irreversible. */
|
|
186
|
+
export async function deleteOrg(orgId: string): Promise<{ deleted: boolean }> {
|
|
187
|
+
return req<{ deleted: boolean }>("DELETE", `/api/auth/orgs/${orgId}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
157
190
|
export interface OrgMember {
|
|
158
191
|
user_id: string;
|
|
159
192
|
role: string;
|
|
160
193
|
joined_at: number;
|
|
194
|
+
/** Member's email, joined server-side from the User entity. `null` if the
|
|
195
|
+
* User row is missing or has no email. */
|
|
196
|
+
email: string | null;
|
|
197
|
+
/** Member's display name (displayName / name / fullName), if set. */
|
|
198
|
+
name: string | null;
|
|
161
199
|
}
|
|
162
200
|
|
|
163
201
|
export async function listOrgMembers(orgId: string): Promise<OrgMember[]> {
|
|
@@ -345,7 +383,7 @@ export async function revokeApiKey(
|
|
|
345
383
|
}
|
|
346
384
|
|
|
347
385
|
async function req<T>(
|
|
348
|
-
method: "PUT" | "DELETE",
|
|
386
|
+
method: "PUT" | "PATCH" | "DELETE",
|
|
349
387
|
path: string,
|
|
350
388
|
body?: unknown,
|
|
351
389
|
): Promise<T> {
|
|
@@ -355,13 +393,7 @@ async function req<T>(
|
|
|
355
393
|
headers: body ? { "content-type": "application/json" } : undefined,
|
|
356
394
|
body: body ? JSON.stringify(body) : undefined,
|
|
357
395
|
});
|
|
358
|
-
if (!res.ok)
|
|
359
|
-
const payload = await res.json().catch(() => ({}) as Record<string, unknown>);
|
|
360
|
-
const code = (payload?.error as string) ?? `HTTP_${res.status}`;
|
|
361
|
-
const message =
|
|
362
|
-
(payload?.message as string) ?? res.statusText ?? "request failed";
|
|
363
|
-
throw new ApiError(code, message, res.status);
|
|
364
|
-
}
|
|
396
|
+
if (!res.ok) await throwApiError(res);
|
|
365
397
|
return res.json() as Promise<T>;
|
|
366
398
|
}
|
|
367
399
|
|