@odla-ai/chapter 0.7.0 → 0.9.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/README.md +82 -18
- package/dist/chunk-YO6DY5DL.js +281 -0
- package/dist/chunk-YO6DY5DL.js.map +1 -0
- package/dist/chunk-ZCZK6QLC.js +537 -0
- package/dist/chunk-ZCZK6QLC.js.map +1 -0
- package/dist/index.cjs +35 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -9
- package/dist/index.d.ts +46 -9
- package/dist/index.js +35 -5
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +71 -0
- package/dist/ui/admin/index.js +9 -0
- package/dist/ui/admin/index.js.map +1 -0
- package/dist/ui/index.d.ts +4 -219
- package/dist/ui/index.js +20 -795
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/member/index.d.ts +152 -0
- package/dist/ui/member/index.js +33 -0
- package/dist/ui/member/index.js.map +1 -0
- package/dist/worker/index.cjs +34 -6
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +9 -0
- package/dist/worker/index.d.ts +9 -0
- package/dist/worker/index.js +34 -6
- package/dist/worker/index.js.map +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -55,11 +55,21 @@ npm i @odla-ai/chapter
|
|
|
55
55
|
the crm namespaces + the chapter namespaces (`applications`, `groups`,
|
|
56
56
|
`meetings`, `emailLog`, plus the auth tables) + a guarded group-row seed. Drop
|
|
57
57
|
it in `odla.config.mjs` `integrations: [...]`.
|
|
58
|
-
- **The UI kit
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
- **The UI kit, adoptable in pieces.** Three entries, split on the dependency
|
|
59
|
+
boundary so you never pay for what you don't use:
|
|
60
|
+
- `@odla-ai/chapter/ui/member` — **react-only**: `SlotPicker`, the date/timezone
|
|
61
|
+
helpers, `JoinIsland` (form → payment → booking), `MembersArea`,
|
|
62
|
+
`Rescheduler`, `PaymentStep`, `<BrandStyle>`. It never imports
|
|
63
|
+
`@odla-ai/auth-clerk` or `@odla-ai/crm`, so you can adopt a single
|
|
64
|
+
presentational component without the Clerk browser SDK. (A bundle-graph test
|
|
65
|
+
enforces this.)
|
|
66
|
+
- `@odla-ai/chapter/ui/admin` — the Clerk-gated console `ChapterAdmin` + the
|
|
67
|
+
section catalog (`peopleSection`). This one does need auth-clerk + crm/ui.
|
|
68
|
+
- `@odla-ai/chapter/ui` — the full barrel, for back-compat.
|
|
69
|
+
|
|
70
|
+
Authored against React, rendered as Preact via `preact/compat` in the reference
|
|
71
|
+
sites. Brand tokens (`brandTokens`/`<BrandStyle>`) re-skin all of it from
|
|
72
|
+
`brand`.
|
|
63
73
|
|
|
64
74
|
## Quick start
|
|
65
75
|
|
|
@@ -97,18 +107,72 @@ export default {
|
|
|
97
107
|
|
|
98
108
|
## Adopting into an existing site
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
A real conversion (the site this was extracted from) went from a 2,094-line
|
|
111
|
+
worker to 6 lines and deleted ~2,500 lines. The order that worked:
|
|
112
|
+
|
|
113
|
+
1. **Config first, assert parity BEFORE deleting anything.** `defineChapter()`
|
|
114
|
+
your site, then diff `chapter.schema` against your existing schema in a test
|
|
115
|
+
and require byte-equality. That single assertion is what makes the deletion
|
|
116
|
+
safe rather than hopeful.
|
|
117
|
+
2. **Freeze the old schema as a test fixture** (e.g. `test/fixtures/legacy-schema.mjs`)
|
|
118
|
+
and keep asserting against it, so an upstream default change fails a test
|
|
119
|
+
instead of a live `provision`. It is the only durable guard against drift in a
|
|
120
|
+
generated schema.
|
|
121
|
+
3. **Swap provisioning** — `createChapterIntegration(chapter)`, inert until the
|
|
122
|
+
next provision run. It supplies schema + rules + seeds, so your
|
|
123
|
+
`odla.config.mjs` `db` block goes away entirely.
|
|
124
|
+
4. **Then the worker**, keeping every bespoke route as a host route
|
|
125
|
+
(`chapterWorker({ chapter, routes })`). Don't hand routes to chapter in the
|
|
126
|
+
same change as the framework swap.
|
|
127
|
+
5. **Override rather than inherit** wherever local behavior was a decision.
|
|
128
|
+
|
|
129
|
+
### Behavior deltas to audit
|
|
130
|
+
|
|
131
|
+
These bite silently — a smoke test won't catch them:
|
|
132
|
+
|
|
133
|
+
- **Per-field caps.** `application.defaultMaxLen` is 2000. If your form accepts
|
|
134
|
+
longer input, pass `maxLen` explicitly or the default starts rejecting it.
|
|
135
|
+
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
136
|
+
against the platform, so set `services` explicitly if you don't run the o11y
|
|
137
|
+
collector.
|
|
138
|
+
- **Which email fires from which route.** `adminNotification` fires from
|
|
139
|
+
`POST /api/applications` (on submit), `prepEmail` from `/api/schedule/book`,
|
|
140
|
+
`paymentConfirmation` from the Stripe webhook. If your policy differs (e.g.
|
|
141
|
+
notify on payment, not submit), override that route.
|
|
142
|
+
- **Account model.** `account: "invite"` (default) mints a Clerk invitation,
|
|
143
|
+
`"create"` makes the account server-side (so join can say the account is
|
|
144
|
+
ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
|
|
145
|
+
site that wants server-side create must set `account: "create"`; inheriting the
|
|
146
|
+
default silently changes the model.
|
|
147
|
+
- **CRM projection points.** chapter projects the person on application submit
|
|
148
|
+
(`projectApplicant`), not on booking or on webhook status change. If you mirror
|
|
149
|
+
pipeline stage into the CRM, keep those routes.
|
|
150
|
+
- **Route names.** chapter serves `/api/config`, `/api/join-config`, etc. Alias
|
|
151
|
+
legacy names in ~4 lines with a host route in `chapterWorker({ routes })` rather
|
|
152
|
+
than rewriting pages.
|
|
153
|
+
|
|
154
|
+
### Install + scope notes
|
|
155
|
+
|
|
156
|
+
- **`@odla-ai/auth-clerk` is only needed for the admin console.** The worker never
|
|
157
|
+
imports it (JWTs are verified with `jose` via `ctx.verifyUser`), and neither
|
|
158
|
+
does `@odla-ai/chapter/ui/member`. Install it when you adopt
|
|
159
|
+
`@odla-ai/chapter/ui/admin` (or want the themed sign-in components); reach for
|
|
160
|
+
`@odla-ai/auth-clerk/invitations` when you want to send your own branded
|
|
161
|
+
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls it, so
|
|
162
|
+
prefer the narrower entry.
|
|
163
|
+
- **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
|
|
164
|
+
the peer conflict that tells you a pair is unsupported. If you need it, find out
|
|
165
|
+
why first.
|
|
166
|
+
- **The admin surface is intentionally minimal** — `/api/admin/scheduling` +
|
|
167
|
+
`/api/admin/meetings`, plus `peopleSection`. Dashboard, billing, per-person
|
|
168
|
+
comms, approve/refund, etc. are your own host routes via the seam; don't plan
|
|
169
|
+
around them shipping.
|
|
170
|
+
|
|
171
|
+
### Verify from the types, not this file
|
|
172
|
+
|
|
173
|
+
At several releases a day, prose lags. Treat this README as intent and verify the
|
|
174
|
+
real surface from `dist/*.d.ts` and by grepping the built bundle for route
|
|
175
|
+
strings. One testing gotcha: Clerk session tokens expire in ~60s, so a script
|
|
176
|
+
that mints a JWT then runs a batch of curls must re-mint per batch.
|
|
113
177
|
|
|
114
178
|
MIT © odla
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// src/ui/admin.tsx
|
|
2
|
+
import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
|
|
3
|
+
import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
|
|
4
|
+
import { CrmClient } from "@odla-ai/crm";
|
|
5
|
+
|
|
6
|
+
// src/ui/chrome.tsx
|
|
7
|
+
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { TopBar, TopBarLink } from "@odla-ai/ui/components";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
|
|
11
|
+
var brandDisplay = (brand) => brand.wordmark ?? brand.name;
|
|
12
|
+
var S = {
|
|
13
|
+
gate: {
|
|
14
|
+
minHeight: "100vh",
|
|
15
|
+
display: "grid",
|
|
16
|
+
placeItems: "center",
|
|
17
|
+
padding: 24,
|
|
18
|
+
background: "radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%), radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)"
|
|
19
|
+
},
|
|
20
|
+
gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
|
|
21
|
+
brandBox: { textAlign: "center", marginBottom: 22 },
|
|
22
|
+
badge: {
|
|
23
|
+
width: 52,
|
|
24
|
+
height: 52,
|
|
25
|
+
margin: "0 auto 14px",
|
|
26
|
+
display: "grid",
|
|
27
|
+
placeItems: "center",
|
|
28
|
+
fontSize: 15,
|
|
29
|
+
fontWeight: 700,
|
|
30
|
+
color: "var(--ui-on-accent)",
|
|
31
|
+
background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
|
|
32
|
+
borderRadius: 14,
|
|
33
|
+
boxShadow: "0 10px 28px var(--ui-accent-soft)"
|
|
34
|
+
},
|
|
35
|
+
badgeSm: {
|
|
36
|
+
width: 26,
|
|
37
|
+
height: 26,
|
|
38
|
+
display: "grid",
|
|
39
|
+
placeItems: "center",
|
|
40
|
+
fontSize: 10,
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
color: "var(--ui-on-accent)",
|
|
43
|
+
background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
|
|
44
|
+
borderRadius: 7,
|
|
45
|
+
marginRight: 9
|
|
46
|
+
},
|
|
47
|
+
h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
|
|
48
|
+
muted: { color: "var(--ui-text-muted)" },
|
|
49
|
+
role: {
|
|
50
|
+
fontFamily: "var(--ui-font-mono)",
|
|
51
|
+
fontSize: 11,
|
|
52
|
+
textTransform: "uppercase",
|
|
53
|
+
letterSpacing: "0.04em",
|
|
54
|
+
padding: "2px 8px",
|
|
55
|
+
borderRadius: 999,
|
|
56
|
+
background: "var(--ui-accent-soft)",
|
|
57
|
+
border: "1px solid var(--ui-accent)",
|
|
58
|
+
color: "var(--ui-accent-strong)"
|
|
59
|
+
},
|
|
60
|
+
whoami: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
|
|
61
|
+
// Sections own their own width/padding (e.g. a .wrap container).
|
|
62
|
+
main: { minHeight: "calc(100vh - 61px)" },
|
|
63
|
+
card: { width: "100%", textAlign: "center" }
|
|
64
|
+
};
|
|
65
|
+
function Gate(props) {
|
|
66
|
+
const { brand, tagline, children } = props;
|
|
67
|
+
return /* @__PURE__ */ jsx("div", { style: S.gate, children: /* @__PURE__ */ jsxs("div", { style: S.gateInner, children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { style: S.brandBox, children: [
|
|
69
|
+
/* @__PURE__ */ jsx("div", { style: S.badge, children: badgeText(brand) }),
|
|
70
|
+
/* @__PURE__ */ jsx("h1", { style: S.h1, children: brandDisplay(brand) }),
|
|
71
|
+
tagline ? /* @__PURE__ */ jsx("p", { style: { ...S.muted, margin: "8px 0 0", fontSize: 14 }, children: tagline }) : null
|
|
72
|
+
] }),
|
|
73
|
+
children
|
|
74
|
+
] }) });
|
|
75
|
+
}
|
|
76
|
+
function AdminShell(props) {
|
|
77
|
+
const { sections, basePath, brand, client, getToken, signOut, email } = props;
|
|
78
|
+
const sectionFromPath = useCallback(() => {
|
|
79
|
+
const fallback = sections[0]?.id ?? "";
|
|
80
|
+
if (typeof window === "undefined") return fallback;
|
|
81
|
+
const rest = window.location.pathname.slice(basePath.length).replace(/^\//, "");
|
|
82
|
+
const id = rest.split("/")[0] ?? "";
|
|
83
|
+
return sections.some((s) => s.id === id) ? id : fallback;
|
|
84
|
+
}, [sections, basePath]);
|
|
85
|
+
const [section, setSection] = useState(sectionFromPath);
|
|
86
|
+
const go = useCallback(
|
|
87
|
+
(id) => {
|
|
88
|
+
window.history.pushState(null, "", `${basePath}/${id}`);
|
|
89
|
+
setSection(id);
|
|
90
|
+
},
|
|
91
|
+
[basePath]
|
|
92
|
+
);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const onPop = () => setSection(sectionFromPath());
|
|
95
|
+
window.addEventListener("popstate", onPop);
|
|
96
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
97
|
+
}, [sectionFromPath]);
|
|
98
|
+
const active = sections.find((s) => s.id === section) ?? sections[0];
|
|
99
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
/* @__PURE__ */ jsxs(TopBar, { children: [
|
|
101
|
+
/* @__PURE__ */ jsxs("a", { className: "topbar-brand", href: "/", style: { display: "inline-flex", alignItems: "center" }, children: [
|
|
102
|
+
/* @__PURE__ */ jsx("span", { style: S.badgeSm, children: badgeText(brand) }),
|
|
103
|
+
brandDisplay(brand)
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsx("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: sections.map((s) => /* @__PURE__ */ jsx(TopBarLink, { active: section === s.id, onClick: () => go(s.id), children: s.label }, s.id)) }),
|
|
106
|
+
/* @__PURE__ */ jsx("div", { className: "topbar-spacer" }),
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { className: "topbar-actions", children: [
|
|
108
|
+
/* @__PURE__ */ jsxs("span", { style: S.whoami, children: [
|
|
109
|
+
/* @__PURE__ */ jsx("span", { style: S.role, children: "admin" }),
|
|
110
|
+
email ? /* @__PURE__ */ jsx("span", { style: S.muted, children: email }) : null
|
|
111
|
+
] }),
|
|
112
|
+
/* @__PURE__ */ jsx("button", { className: "btn secondary mini", onClick: () => signOut(), children: "Sign out" })
|
|
113
|
+
] })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsx("main", { className: "shell-main", style: S.main, children: active ? active.render({ client, getToken, navigate: go }) : null })
|
|
116
|
+
] });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/ui/admin.tsx
|
|
120
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
121
|
+
function Authed(props) {
|
|
122
|
+
const { sections, basePath, brand, crmBasePath, apiBase } = props;
|
|
123
|
+
const { getToken, signOut } = useClerkAuth();
|
|
124
|
+
const client = useMemo(
|
|
125
|
+
() => new CrmClient({
|
|
126
|
+
basePath: crmBasePath,
|
|
127
|
+
headers: async () => {
|
|
128
|
+
const t = await getToken();
|
|
129
|
+
return t ? { authorization: `Bearer ${t}` } : {};
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
[getToken, crmBasePath]
|
|
133
|
+
);
|
|
134
|
+
const [state, setState] = useState2({
|
|
135
|
+
status: "checking"
|
|
136
|
+
});
|
|
137
|
+
useEffect2(() => {
|
|
138
|
+
let live = true;
|
|
139
|
+
void (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const t = await getToken();
|
|
142
|
+
const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
|
|
143
|
+
const body = await res.json().catch(() => ({}));
|
|
144
|
+
if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
|
|
145
|
+
} catch {
|
|
146
|
+
if (live) setState({ status: "denied" });
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
return () => {
|
|
150
|
+
live = false;
|
|
151
|
+
};
|
|
152
|
+
}, [getToken, apiBase]);
|
|
153
|
+
if (state.status === "checking") {
|
|
154
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Checking access\u2026" }) });
|
|
155
|
+
}
|
|
156
|
+
if (state.status === "denied") {
|
|
157
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
|
|
158
|
+
/* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
|
|
159
|
+
/* @__PURE__ */ jsxs2("p", { style: S.muted, children: [
|
|
160
|
+
state.email ? `${state.email} isn't` : "This account isn't",
|
|
161
|
+
" on the admin list. Ask an existing admin to add you in odla Studio."
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsx2("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
|
|
164
|
+
] }) });
|
|
165
|
+
}
|
|
166
|
+
return /* @__PURE__ */ jsx2(
|
|
167
|
+
AdminShell,
|
|
168
|
+
{
|
|
169
|
+
sections,
|
|
170
|
+
basePath,
|
|
171
|
+
brand,
|
|
172
|
+
client,
|
|
173
|
+
getToken,
|
|
174
|
+
signOut,
|
|
175
|
+
email: state.email ?? null
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function ChapterAdmin(props) {
|
|
180
|
+
const sections = props.sections;
|
|
181
|
+
const basePath = props.basePath ?? "/admin";
|
|
182
|
+
const brand = props.brand ?? { name: "Admin" };
|
|
183
|
+
const crmBasePath = props.crmBasePath ?? "/api/crm";
|
|
184
|
+
const apiBase = props.apiBase ?? "";
|
|
185
|
+
const [pk, setPk] = useState2(void 0);
|
|
186
|
+
useEffect2(() => {
|
|
187
|
+
let live = true;
|
|
188
|
+
void (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`${apiBase}/api/config`);
|
|
191
|
+
const body = await res.json();
|
|
192
|
+
if (live) setPk(body.clerkPublishableKey ?? null);
|
|
193
|
+
} catch {
|
|
194
|
+
if (live) setPk(null);
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
return () => {
|
|
198
|
+
live = false;
|
|
199
|
+
};
|
|
200
|
+
}, [apiBase]);
|
|
201
|
+
if (pk === void 0) {
|
|
202
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Loading\u2026" }) });
|
|
203
|
+
}
|
|
204
|
+
if (!pk) {
|
|
205
|
+
return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
|
|
206
|
+
/* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
|
|
207
|
+
/* @__PURE__ */ jsx2("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
|
|
208
|
+
] }) });
|
|
209
|
+
}
|
|
210
|
+
return /* @__PURE__ */ jsxs2(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
|
|
211
|
+
/* @__PURE__ */ jsx2(SignedOut, { children: /* @__PURE__ */ jsx2(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx2(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
|
|
212
|
+
/* @__PURE__ */ jsx2(SignedIn, { children: /* @__PURE__ */ jsx2(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/ui/admin-people.tsx
|
|
217
|
+
import { useState as useState3 } from "react";
|
|
218
|
+
import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
|
|
219
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
220
|
+
function PeopleBody(props) {
|
|
221
|
+
const { crm, client, type } = props;
|
|
222
|
+
const hookClient = client;
|
|
223
|
+
const query = useCrmQuery(hookClient, type);
|
|
224
|
+
const [openId, setOpenId] = useState3(null);
|
|
225
|
+
const record = useCrmRecord(hookClient, openId);
|
|
226
|
+
const [saving, setSaving] = useState3(false);
|
|
227
|
+
const saveFields = async (input) => {
|
|
228
|
+
if (!openId) return;
|
|
229
|
+
setSaving(true);
|
|
230
|
+
try {
|
|
231
|
+
await client.updateRecord(openId, { input });
|
|
232
|
+
record.refresh();
|
|
233
|
+
query.refresh();
|
|
234
|
+
} finally {
|
|
235
|
+
setSaving(false);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const moveStage = async (to) => {
|
|
239
|
+
if (!openId) return;
|
|
240
|
+
await client.setStage(openId, to);
|
|
241
|
+
record.refresh();
|
|
242
|
+
query.refresh();
|
|
243
|
+
};
|
|
244
|
+
const addTag = async (tag) => {
|
|
245
|
+
if (openId) {
|
|
246
|
+
await client.addTag(openId, tag);
|
|
247
|
+
record.refresh();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const removeTag = async (tag) => {
|
|
251
|
+
if (openId) {
|
|
252
|
+
await client.removeTag(openId, tag);
|
|
253
|
+
record.refresh();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return /* @__PURE__ */ jsxs3("div", { className: "wrap", children: [
|
|
257
|
+
/* @__PURE__ */ jsx3(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
|
|
258
|
+
record.detail ? /* @__PURE__ */ jsx3(
|
|
259
|
+
RecordPanel,
|
|
260
|
+
{
|
|
261
|
+
crm,
|
|
262
|
+
detail: record.detail,
|
|
263
|
+
onSaveFields: saveFields,
|
|
264
|
+
onMoveStage: moveStage,
|
|
265
|
+
onAddTag: addTag,
|
|
266
|
+
onRemoveTag: removeTag,
|
|
267
|
+
saving
|
|
268
|
+
}
|
|
269
|
+
) : null
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
function peopleSection(options) {
|
|
273
|
+
const { crm, id = "people", label = "People", type = "person" } = options;
|
|
274
|
+
return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(PeopleBody, { crm, client: ctx.client, type }) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export {
|
|
278
|
+
ChapterAdmin,
|
|
279
|
+
peopleSection
|
|
280
|
+
};
|
|
281
|
+
//# sourceMappingURL=chunk-YO6DY5DL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ui/admin.tsx","../src/ui/chrome.tsx","../src/ui/admin-people.tsx"],"sourcesContent":["// ChapterAdmin — the whole admin console for a chapter/hub, owned by the package\n// so no site re-wires (or re-bugs) it: the Clerk publishable-key fetch\n// (/api/config), the themed sign-in with the post-sign-in redirect DERIVED from\n// basePath (so it never bounces to \"/\"), the odla-db admins-allowlist check\n// (/api/me), and a TopBar shell (chrome.tsx) that renders the caller's sections.\nimport { useEffect, useMemo, useState } from \"react\";\nimport { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from \"@odla-ai/auth-clerk\";\nimport { CrmClient } from \"@odla-ai/crm\";\nimport { S, Gate, AdminShell } from \"./chrome.js\";\nimport type { AdminSection, Brand } from \"./chrome.js\";\n\nexport type { AdminSection, AdminSectionContext } from \"./chrome.js\";\n\n/** Props for {@link ChapterAdmin}. */\nexport interface ChapterAdminProps {\n /** The admin sections to render (nav label + body). */\n sections: AdminSection[];\n /** Admin mount path. The sign-in redirect + section routing derive from it,\n * so the \"bounce to home after sign-in\" bug is impossible. Default \"/admin\". */\n basePath?: string;\n /** Wordmark shown in the gate + top bar. */\n brand?: Brand;\n /** CRM route mount. Default \"/api/crm\". */\n crmBasePath?: string;\n /** Origin for /api/config and /api/me. Default same origin. */\n apiBase?: string;\n}\n\n// After sign-in: build a token-bound CrmClient, confirm the account is an\n// allowlisted admin (/api/me), then render the shell.\nfunction Authed(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n crmBasePath: string;\n apiBase: string;\n}) {\n const { sections, basePath, brand, crmBasePath, apiBase } = props;\n const { getToken, signOut } = useClerkAuth();\n const client = useMemo(\n () =>\n new CrmClient({\n basePath: crmBasePath,\n headers: async (): Promise<Record<string, string>> => {\n const t = await getToken();\n return t ? { authorization: `Bearer ${t}` } : {};\n },\n }),\n [getToken, crmBasePath],\n );\n const [state, setState] = useState<{ status: \"checking\" | \"ok\" | \"denied\"; email?: string | null }>({\n status: \"checking\",\n });\n\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const t = await getToken();\n const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });\n const body = (await res.json().catch(() => ({}))) as { authorized?: boolean; email?: string | null };\n if (live) setState({ status: body.authorized ? \"ok\" : \"denied\", email: body.email ?? null });\n } catch {\n if (live) setState({ status: \"denied\" });\n }\n })();\n return () => {\n live = false;\n };\n }, [getToken, apiBase]);\n\n if (state.status === \"checking\") {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Checking access…</p>\n </Gate>\n );\n }\n if (state.status === \"denied\") {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Not authorized</h2>\n <p style={S.muted}>\n {state.email ? `${state.email} isn't` : \"This account isn't\"} on the admin list. Ask an existing admin to add\n you in odla Studio.\n </p>\n <button className=\"btn secondary\" onClick={() => signOut()} style={{ marginTop: 12 }}>\n Sign out\n </button>\n </div>\n </Gate>\n );\n }\n return (\n <AdminShell\n sections={sections}\n basePath={basePath}\n brand={brand}\n client={client}\n getToken={getToken}\n signOut={signOut}\n email={state.email ?? null}\n />\n );\n}\n\n/**\n * The whole admin console for a chapter/hub: fetches the Clerk publishable key\n * (/api/config), gates on sign-in with the post-sign-in redirect derived from\n * `basePath`, checks the odla-db admins allowlist (/api/me), and renders a\n * TopBar shell over the caller's `sections`. The gate/shell/redirect are owned\n * here so no site re-wires (or re-bugs) them.\n */\nexport function ChapterAdmin(props: ChapterAdminProps) {\n const sections = props.sections;\n const basePath = props.basePath ?? \"/admin\";\n const brand = props.brand ?? { name: \"Admin\" };\n const crmBasePath = props.crmBasePath ?? \"/api/crm\";\n const apiBase = props.apiBase ?? \"\";\n\n const [pk, setPk] = useState<string | null | undefined>(undefined);\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const res = await fetch(`${apiBase}/api/config`);\n const body = (await res.json()) as { clerkPublishableKey?: string | null };\n if (live) setPk(body.clerkPublishableKey ?? null);\n } catch {\n if (live) setPk(null);\n }\n })();\n return () => {\n live = false;\n };\n }, [apiBase]);\n\n if (pk === undefined) {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Loading…</p>\n </Gate>\n );\n }\n if (!pk) {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Sign-in not configured</h2>\n <p style={S.muted}>No Clerk publishable key is set for this environment yet.</p>\n </div>\n </Gate>\n );\n }\n return (\n <ClerkGate publishableKey={pk} appearance={clerkAppearanceFromTokens()} afterSignOutUrl=\"/\">\n <SignedOut>\n <Gate brand={brand} tagline=\"Admin sign-in — invite only\">\n <SignIn routing=\"hash\" forceRedirectUrl={basePath} signUpForceRedirectUrl={basePath} />\n </Gate>\n </SignedOut>\n <SignedIn>\n <Authed sections={sections} basePath={basePath} brand={brand} crmBasePath={crmBasePath} apiBase={apiBase} />\n </SignedIn>\n </ClerkGate>\n );\n}\n","// Shared chrome for ChapterAdmin: the section contract, the centered gate\n// screen, the signed-in TopBar shell, and the inline style tokens. Split out of\n// admin.tsx to stay under the repo's per-file LOC cap. Uses only @odla-ai/ui\n// classes + --ui-* tokens, so a consumer needs nothing beyond the odla-ui sheet.\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { TopBar, TopBarLink } from \"@odla-ai/ui/components\";\nimport type { CrmClient } from \"@odla-ai/crm\";\n\n/** What each admin section receives. `client` is a CrmClient bound to the\n * signed-in admin's bearer token; `navigate` switches sections. */\nexport interface AdminSectionContext {\n client: CrmClient;\n getToken: () => Promise<string | null>;\n /** Switch to another section by id (e.g. Overview \"jump in\" buttons). */\n navigate: (sectionId: string) => void;\n}\n\n/** One admin-console section: a nav label and a body renderer. */\nexport interface AdminSection {\n id: string;\n label: string;\n render: (ctx: AdminSectionContext) => ReactNode;\n}\n\n/** Wordmark shown in the gate and top bar. `name` is the plain-text identity (and\n * the badge fallback); `wordmark` is an optional rich display node for brands\n * whose typography can't be a plain string (e.g. an ampersand wrapped to render\n * upright), and `badge` overrides the glyph. */\nexport type Brand = { name: string; badge?: string; wordmark?: ReactNode };\n\n/** The badge glyph: the explicit `badge`, else the first 3 letters of `name`. */\nexport const badgeText = (brand: Brand): string => brand.badge ?? brand.name.slice(0, 3).toUpperCase();\n\n/** The display node: the rich `wordmark` if given, else the plain `name`. */\nexport const brandDisplay = (brand: Brand): ReactNode => brand.wordmark ?? brand.name;\n\n/** Inline style tokens (keyed to --ui-* custom properties). */\nexport const S: Record<string, CSSProperties> = {\n gate: {\n minHeight: \"100vh\",\n display: \"grid\",\n placeItems: \"center\",\n padding: 24,\n background:\n \"radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%),\" +\n \" radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)\",\n },\n gateInner: { width: \"100%\", maxWidth: 400, display: \"flex\", flexDirection: \"column\", alignItems: \"center\" },\n brandBox: { textAlign: \"center\", marginBottom: 22 },\n badge: {\n width: 52,\n height: 52,\n margin: \"0 auto 14px\",\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 15,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 14,\n boxShadow: \"0 10px 28px var(--ui-accent-soft)\",\n },\n badgeSm: {\n width: 26,\n height: 26,\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 10,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 7,\n marginRight: 9,\n },\n h1: { margin: 0, fontSize: 28, letterSpacing: \"-0.02em\", fontWeight: 700 },\n muted: { color: \"var(--ui-text-muted)\" },\n role: {\n fontFamily: \"var(--ui-font-mono)\",\n fontSize: 11,\n textTransform: \"uppercase\",\n letterSpacing: \"0.04em\",\n padding: \"2px 8px\",\n borderRadius: 999,\n background: \"var(--ui-accent-soft)\",\n border: \"1px solid var(--ui-accent)\",\n color: \"var(--ui-accent-strong)\",\n },\n whoami: { display: \"flex\", alignItems: \"center\", gap: 8, fontSize: 12 },\n // Sections own their own width/padding (e.g. a .wrap container).\n main: { minHeight: \"calc(100vh - 61px)\" },\n card: { width: \"100%\", textAlign: \"center\" },\n};\n\n/** Centered gate screen used for loading / sign-in / denied / not-configured. */\nexport function Gate(props: { brand: Brand; tagline?: string; children: ReactNode }) {\n const { brand, tagline, children } = props;\n return (\n <div style={S.gate}>\n <div style={S.gateInner}>\n <div style={S.brandBox}>\n <div style={S.badge}>{badgeText(brand)}</div>\n <h1 style={S.h1}>{brandDisplay(brand)}</h1>\n {tagline ? <p style={{ ...S.muted, margin: \"8px 0 0\", fontSize: 14 }}>{tagline}</p> : null}\n </div>\n {children}\n </div>\n </div>\n );\n}\n\n/** The signed-in shell: a TopBar of the caller's sections + the active body. */\nexport function AdminShell(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n client: CrmClient;\n getToken: () => Promise<string | null>;\n signOut: () => void;\n email: string | null;\n}) {\n const { sections, basePath, brand, client, getToken, signOut, email } = props;\n\n const sectionFromPath = useCallback(() => {\n const fallback = sections[0]?.id ?? \"\";\n if (typeof window === \"undefined\") return fallback;\n const rest = window.location.pathname.slice(basePath.length).replace(/^\\//, \"\");\n const id = rest.split(\"/\")[0] ?? \"\";\n return sections.some((s) => s.id === id) ? id : fallback;\n }, [sections, basePath]);\n\n const [section, setSection] = useState(sectionFromPath);\n\n const go = useCallback(\n (id: string) => {\n window.history.pushState(null, \"\", `${basePath}/${id}`);\n setSection(id);\n },\n [basePath],\n );\n\n useEffect(() => {\n const onPop = () => setSection(sectionFromPath());\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, [sectionFromPath]);\n\n const active = sections.find((s) => s.id === section) ?? sections[0];\n\n return (\n <>\n <TopBar>\n <a className=\"topbar-brand\" href=\"/\" style={{ display: \"inline-flex\", alignItems: \"center\" }}>\n <span style={S.badgeSm}>{badgeText(brand)}</span>\n {brandDisplay(brand)}\n </a>\n <nav className=\"topbar-nav\" aria-label=\"Admin navigation\">\n {sections.map((s) => (\n <TopBarLink key={s.id} active={section === s.id} onClick={() => go(s.id)}>\n {s.label}\n </TopBarLink>\n ))}\n </nav>\n <div className=\"topbar-spacer\" />\n <div className=\"topbar-actions\">\n <span style={S.whoami}>\n <span style={S.role}>admin</span>\n {email ? <span style={S.muted}>{email}</span> : null}\n </span>\n <button className=\"btn secondary mini\" onClick={() => signOut()}>\n Sign out\n </button>\n </div>\n </TopBar>\n <main className=\"shell-main\" style={S.main}>\n {active ? active.render({ client, getToken, navigate: go }) : null}\n </main>\n </>\n );\n}\n","// People/CRM admin section — the core admin workflow, assembled from the shipped\n// @odla-ai/crm/ui kit against the /api/crm/* routes the worker already mounts.\n// It's a FACTORY: peopleSection({ crm }) → an AdminSection you drop into\n// ChapterAdmin's `sections`. Every lifecycle action (edit fields, move pipeline\n// stage, tag) goes through the token-bound CrmClient — never a direct db write,\n// so the package's operational invariants hold (G7).\nimport { useState } from \"react\";\nimport { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from \"@odla-ai/crm/ui\";\nimport type { Crm, CrmClient, CrmRecord } from \"@odla-ai/crm\";\nimport type { AdminSection } from \"./chrome.js\";\n\nfunction PeopleBody(props: { crm: Crm; client: CrmClient; type: string }) {\n const { crm, client, type } = props;\n // The crm/ui hooks type their client against @odla-ai/crm's ui-entry bundled\n // declaration; ctx.client is the core-entry declaration. Same runtime class,\n // but their private field makes the two .d.ts copies nominally distinct — cast\n // to the param type the hooks expect.\n const hookClient = client as unknown as Parameters<typeof useCrmQuery>[0];\n const query = useCrmQuery(hookClient, type);\n const [openId, setOpenId] = useState<string | null>(null);\n const record = useCrmRecord(hookClient, openId);\n const [saving, setSaving] = useState(false);\n\n const saveFields = async (input: Record<string, unknown>) => {\n if (!openId) return;\n setSaving(true);\n try {\n await client.updateRecord(openId, { input });\n record.refresh();\n query.refresh();\n } finally {\n setSaving(false);\n }\n };\n const moveStage = async (to: string) => {\n if (!openId) return;\n await client.setStage(openId, to);\n record.refresh();\n query.refresh();\n };\n const addTag = async (tag: string) => {\n if (openId) {\n await client.addTag(openId, tag);\n record.refresh();\n }\n };\n const removeTag = async (tag: string) => {\n if (openId) {\n await client.removeTag(openId, tag);\n record.refresh();\n }\n };\n\n return (\n <div className=\"wrap\">\n <CrmList crm={crm} type={type} query={query} onOpenRecord={(r: CrmRecord) => setOpenId(r.id)} />\n {record.detail ? (\n <RecordPanel\n crm={crm}\n detail={record.detail}\n onSaveFields={saveFields}\n onMoveStage={moveStage}\n onAddTag={addTag}\n onRemoveTag={removeTag}\n saving={saving}\n />\n ) : null}\n </div>\n );\n}\n\n/** Options for {@link peopleSection}. */\nexport interface PeopleSectionOptions {\n /** The chapter's resolved CRM engine — `chapter.crm` from defineChapter. */\n crm: Crm;\n /** Section id / route segment. Default \"people\". */\n id?: string;\n /** Nav label. Default \"People\". */\n label?: string;\n /** The record type to list. Default \"person\". */\n type?: string;\n}\n\n/**\n * Build the People/CRM admin section: a server-driven records table plus a\n * facet-composed detail drawer (fields, pipeline stage, tags) over the\n * token-bound CrmClient. Drop the result into `ChapterAdmin`'s `sections`.\n * (Notes/activity feed and saved-view navigation are noted follow-ups.)\n */\nexport function peopleSection(options: PeopleSectionOptions): AdminSection {\n const { crm, id = \"people\", label = \"People\", type = \"person\" } = options;\n return { id, label, render: (ctx) => <PeopleBody crm={crm} client={ctx.client} type={type} /> };\n}\n"],"mappings":"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","names":["useEffect","useState","jsx","jsxs","useState","useEffect","useState","jsx","jsxs"]}
|