@elytracms/studio 0.0.46 → 0.0.48
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/convex/auth.ts +61 -12
- package/convex/authority.ts +40 -2
- package/convex/functions.ts +9 -0
- package/convex/invites.ts +75 -0
- package/convex/members.ts +44 -18
- package/convex/schema.ts +37 -0
- package/convex/users.ts +89 -2
- package/dist/components/account/account-settings.d.ts +13 -0
- package/dist/components/account/account-settings.js +54 -0
- package/dist/components/account/account-settings.js.map +1 -0
- package/dist/components/account/credential-panel.d.ts +14 -0
- package/dist/components/account/credential-panel.js +52 -0
- package/dist/components/account/credential-panel.js.map +1 -0
- package/dist/components/account/self-account-schema.d.ts +19 -0
- package/dist/components/account/self-account-schema.js +52 -0
- package/dist/components/account/self-account-schema.js.map +1 -0
- package/dist/components/account/self-account-view-source.d.ts +35 -0
- package/dist/components/account/self-account-view-source.js +110 -0
- package/dist/components/account/self-account-view-source.js.map +1 -0
- package/dist/components/ai/ai-chat-panel.js +9 -0
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.js +5 -1
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.js +12 -5
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/translate-document-action.d.ts +14 -0
- package/dist/components/ai/translate-document-action.js +38 -0
- package/dist/components/ai/translate-document-action.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.d.ts +5 -0
- package/dist/components/ai/translate-document-dialog.js +97 -0
- package/dist/components/ai/translate-document-dialog.js.map +1 -0
- package/dist/components/auth-context.d.ts +5 -1
- package/dist/components/auth-context.js +9 -0
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/cms/detail-editor.js +5 -1
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/current-user-menu.js +11 -2
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +31 -0
- package/dist/components/deployment-setup-gate.js +92 -0
- package/dist/components/deployment-setup-gate.js.map +1 -0
- package/dist/components/project-session.d.ts +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +3 -0
- package/dist/components/users/invite-user-bar.js +91 -0
- package/dist/components/users/invite-user-bar.js.map +1 -0
- package/dist/components/users/users-module.js +13 -2
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +9 -1
- package/dist/lib/ai/chat-state.js +2 -0
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/create-flow.d.ts +17 -1
- package/dist/lib/ai/create-flow.js +86 -0
- package/dist/lib/ai/create-flow.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/rich-text-translate.d.ts +45 -0
- package/dist/lib/ai/rich-text-translate.js +229 -0
- package/dist/lib/ai/rich-text-translate.js.map +1 -0
- package/dist/lib/ai/tools.d.ts +13 -1
- package/dist/lib/ai/tools.js +15 -4
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +53 -0
- package/dist/lib/ai/translate-document.js +316 -0
- package/dist/lib/ai/translate-document.js.map +1 -0
- package/dist/lib/auth/auth-adapter.d.ts +50 -2
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +28 -1
- package/dist/lib/auth/convex-auth-adapter.js +113 -1
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +3 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +2 -2
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +3 -1
- package/dist/lib/auth/local-auth-adapter.js +16 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +3 -3
- package/dist/lib/i18n/messages.en.js +68 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +1 -1
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +1 -1
- package/dist/lib/persistence/backend-mode.js +1 -1
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +2 -1
- package/dist/lib/persistence/convex-adapter.js +69 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +36 -0
- package/dist/lib/persistence/deployment-readiness.js +26 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -0
- package/dist/lib/persistence/index.d.ts +3 -1
- package/dist/lib/persistence/index.js +4 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.js +1 -1
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +1 -1
- package/dist/lib/workspace/workspace-source.js +1 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DeploymentReadinessReport } from '@elytracms/core/export-sync/deployment-ready';
|
|
3
|
+
/**
|
|
4
|
+
* Deployment-setup boot gate (EC-258) — the single readiness surface that
|
|
5
|
+
* supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)
|
|
6
|
+
* and `WorkspaceGate` (invalid config) screens by composing the shared
|
|
7
|
+
* `checkDeploymentReady` contract. It renders ONE deterministic "Deployment setup"
|
|
8
|
+
* screen listing each failing precondition with its copy-pasteable fix, instead of
|
|
9
|
+
* letting the app boot into a misleading downstream error (most painfully the
|
|
10
|
+
* Konditorei JWT-key footgun). All checks passing → children render untouched.
|
|
11
|
+
*
|
|
12
|
+
* Runs before auth and any adapter is constructed (it is the thing that keeps
|
|
13
|
+
* `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured
|
|
14
|
+
* backend). Convex mode probes reachability once on the client (`adapter.status()`)
|
|
15
|
+
* with a retry; playground and unconfigured resolve synchronously. SSR renders the
|
|
16
|
+
* deterministic "checking" state. Auth-signing-keys is reported as `unknown` at boot
|
|
17
|
+
* (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error
|
|
18
|
+
* is the browser's actionable surface; `elytra doctor` probes it from node), and the
|
|
19
|
+
* per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.
|
|
20
|
+
*/
|
|
21
|
+
export declare function DeploymentSetupGate({ children }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* The unified "Deployment setup" screen (EC-258): every failing check + its exact
|
|
26
|
+
* fix. Pure/presentational so it is fixture-reproducible and static-render testable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function DeploymentSetupScreen({ report, onRetry, }: {
|
|
29
|
+
report: DeploymentReadinessReport;
|
|
30
|
+
onRetry?: () => void;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { resolveBackendConfig, selectPersistenceAdapter, studioDeploymentReadiness, } from '@elytracms/studio/lib/persistence';
|
|
5
|
+
import { getWorkspaceState, resolveDeploymentCollections, resolveDeploymentUrl, } from '@elytracms/studio/lib/workspace';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
/**
|
|
8
|
+
* Deployment-setup boot gate (EC-258) — the single readiness surface that
|
|
9
|
+
* supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)
|
|
10
|
+
* and `WorkspaceGate` (invalid config) screens by composing the shared
|
|
11
|
+
* `checkDeploymentReady` contract. It renders ONE deterministic "Deployment setup"
|
|
12
|
+
* screen listing each failing precondition with its copy-pasteable fix, instead of
|
|
13
|
+
* letting the app boot into a misleading downstream error (most painfully the
|
|
14
|
+
* Konditorei JWT-key footgun). All checks passing → children render untouched.
|
|
15
|
+
*
|
|
16
|
+
* Runs before auth and any adapter is constructed (it is the thing that keeps
|
|
17
|
+
* `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured
|
|
18
|
+
* backend). Convex mode probes reachability once on the client (`adapter.status()`)
|
|
19
|
+
* with a retry; playground and unconfigured resolve synchronously. SSR renders the
|
|
20
|
+
* deterministic "checking" state. Auth-signing-keys is reported as `unknown` at boot
|
|
21
|
+
* (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error
|
|
22
|
+
* is the browser's actionable surface; `elytra doctor` probes it from node), and the
|
|
23
|
+
* per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.
|
|
24
|
+
*/
|
|
25
|
+
export function DeploymentSetupGate({ children }) {
|
|
26
|
+
const config = useMemo(() => resolveBackendConfig(), []);
|
|
27
|
+
const staticReport = useCallback(() => studioDeploymentReadiness({
|
|
28
|
+
backend: config,
|
|
29
|
+
workspace: getWorkspaceState(),
|
|
30
|
+
collections: resolveDeploymentCollections(),
|
|
31
|
+
}), [config]);
|
|
32
|
+
const [state, setState] = useState(() => config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() });
|
|
33
|
+
const check = useCallback(() => {
|
|
34
|
+
if (config.kind !== 'convex') {
|
|
35
|
+
setState({ phase: 'resolved', report: staticReport() });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const effectiveUrl = resolveDeploymentUrl(config.url);
|
|
39
|
+
const resolve = (reachable) => setState({
|
|
40
|
+
phase: 'resolved',
|
|
41
|
+
report: studioDeploymentReadiness({
|
|
42
|
+
backend: config,
|
|
43
|
+
effectiveUrl,
|
|
44
|
+
workspace: getWorkspaceState(),
|
|
45
|
+
collections: resolveDeploymentCollections(),
|
|
46
|
+
reachable,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
setState({ phase: 'checking' });
|
|
50
|
+
void selectPersistenceAdapter()
|
|
51
|
+
.then((adapter) => adapter.status())
|
|
52
|
+
.then((status) => resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }))
|
|
53
|
+
.catch((error) => resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }));
|
|
54
|
+
}, [config, staticReport]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (config.kind === 'convex')
|
|
57
|
+
check();
|
|
58
|
+
}, [config.kind, check]);
|
|
59
|
+
if (state.phase === 'checking')
|
|
60
|
+
return _jsx(DeploymentChecking, {});
|
|
61
|
+
if (blockingFailures(state.report).length > 0) {
|
|
62
|
+
return (_jsx(DeploymentSetupScreen, { report: state.report, onRetry: config.kind === 'convex' ? check : undefined }));
|
|
63
|
+
}
|
|
64
|
+
return _jsx(_Fragment, { children: children });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The checks that BLOCK boot: a failed deployment- or app-level precondition. The
|
|
68
|
+
* per-user bucket ("not a member") is not a boot blocker — it is surfaced post-auth
|
|
69
|
+
* ("ask an admin"), and at boot it is `unknown` (pre-auth) anyway.
|
|
70
|
+
*/
|
|
71
|
+
function blockingFailures(report) {
|
|
72
|
+
return report.checks.filter((c) => c.status === 'fail' && (c.bucket === 'deployment' || c.bucket === 'app'));
|
|
73
|
+
}
|
|
74
|
+
/** Deterministic boot state while the backend probe is in flight (also SSR output). */
|
|
75
|
+
function DeploymentChecking() {
|
|
76
|
+
return (_jsx(BootScreenFrame, { children: _jsx(Text, { size: "sm", tone: "muted", children: translate('shell.deploymentSetup.checking') }) }));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The unified "Deployment setup" screen (EC-258): every failing check + its exact
|
|
80
|
+
* fix. Pure/presentational so it is fixture-reproducible and static-render testable.
|
|
81
|
+
*/
|
|
82
|
+
export function DeploymentSetupScreen({ report, onRetry, }) {
|
|
83
|
+
const failures = report.checks.filter((c) => c.status === 'fail');
|
|
84
|
+
return (_jsx(BootScreenFrame, { children: _jsxs(Card, { className: "max-w-2xl border-destructive/40", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: translate('shell.deploymentSetup.title') }) }), _jsx(CardContent, { children: _jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", tone: "muted", children: translate('shell.deploymentSetup.description') }), _jsx(Stack, { gap: "2", role: "list", "aria-label": translate('shell.deploymentSetup.issuesLabel'), children: failures.map((check) => (_jsx(CheckRow, { check: check }, check.id))) }), onRetry ? (_jsx(Button, { variant: "outline", size: "sm", onClick: onRetry, className: "self-start", children: translate('shell.deploymentSetup.retry') })) : null] }) })] }) }));
|
|
85
|
+
}
|
|
86
|
+
function CheckRow({ check }) {
|
|
87
|
+
return (_jsxs("div", { role: "listitem", className: "rounded-md border border-destructive/30 bg-destructive/5 p-2", children: [_jsxs(Text, { size: "xs", mono: true, className: "text-destructive", children: ["[", check.id, "]"] }), _jsx(Text, { size: "sm", children: check.detail }), check.fix ? (_jsxs(Text, { size: "xs", tone: "muted", className: "mt-1", children: [translate('shell.deploymentSetup.fixLabel'), ": ", _jsx(Code, { className: "break-all", children: check.fix })] })) : null] }));
|
|
88
|
+
}
|
|
89
|
+
function BootScreenFrame({ children }) {
|
|
90
|
+
return _jsx("div", { className: "grid min-h-dvh place-items-center bg-background p-6", children: children });
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=deployment-setup-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-setup-gate.js","sourceRoot":"","sources":["../../src/components/deployment-setup-gate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Card, CardContent, CardHeader, CardTitle, Code, Stack, Text } from '@elytracms/studio/ui'\nimport {\n resolveBackendConfig,\n selectPersistenceAdapter,\n studioDeploymentReadiness,\n} from '@elytracms/studio/lib/persistence'\nimport {\n getWorkspaceState,\n resolveDeploymentCollections,\n resolveDeploymentUrl,\n} from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type {\n DeploymentReadinessReport,\n ReadinessCheck,\n} from '@elytracms/core/export-sync/deployment-ready'\n\n/**\n * Deployment-setup boot gate (EC-258) — the single readiness surface that\n * supersedes the separate `BackendGate` (missing `VITE_CONVEX_URL` / unreachable)\n * and `WorkspaceGate` (invalid config) screens by composing the shared\n * `checkDeploymentReady` contract. It renders ONE deterministic \"Deployment setup\"\n * screen listing each failing precondition with its copy-pasteable fix, instead of\n * letting the app boot into a misleading downstream error (most painfully the\n * Konditorei JWT-key footgun). All checks passing → children render untouched.\n *\n * Runs before auth and any adapter is constructed (it is the thing that keeps\n * `selectAuthAdapter`/`selectPersistenceAdapter` from throwing on a misconfigured\n * backend). Convex mode probes reachability once on the client (`adapter.status()`)\n * with a retry; playground and unconfigured resolve synchronously. SSR renders the\n * deterministic \"checking\" state. Auth-signing-keys is reported as `unknown` at boot\n * (the JWKS endpoint is CORS-blocked from the browser — EC-257's honest sign-in error\n * is the browser's actionable surface; `elytra doctor` probes it from node), and the\n * per-user bucket stays `unknown` (this gate runs pre-auth), so neither ever blocks boot.\n */\nexport function DeploymentSetupGate({ children }: { children: ReactNode }) {\n const config = useMemo(() => resolveBackendConfig(), [])\n\n const staticReport = useCallback(\n (): DeploymentReadinessReport =>\n studioDeploymentReadiness({\n backend: config,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n }),\n [config],\n )\n\n const [state, setState] = useState<GateState>(() =>\n config.kind === 'convex' ? { phase: 'checking' } : { phase: 'resolved', report: staticReport() },\n )\n\n const check = useCallback(() => {\n if (config.kind !== 'convex') {\n setState({ phase: 'resolved', report: staticReport() })\n return\n }\n const effectiveUrl = resolveDeploymentUrl(config.url)\n const resolve = (reachable: { available: boolean; detail?: string }) =>\n setState({\n phase: 'resolved',\n report: studioDeploymentReadiness({\n backend: config,\n effectiveUrl,\n workspace: getWorkspaceState(),\n collections: resolveDeploymentCollections(),\n reachable,\n }),\n })\n setState({ phase: 'checking' })\n void selectPersistenceAdapter()\n .then((adapter) => adapter.status())\n .then((status) =>\n resolve({ available: status.available, ...(status.detail ? { detail: status.detail } : {}) }),\n )\n .catch((error: unknown) =>\n resolve({ available: false, detail: error instanceof Error ? error.message : String(error) }),\n )\n }, [config, staticReport])\n\n useEffect(() => {\n if (config.kind === 'convex') check()\n }, [config.kind, check])\n\n if (state.phase === 'checking') return <DeploymentChecking />\n if (blockingFailures(state.report).length > 0) {\n return (\n <DeploymentSetupScreen\n report={state.report}\n onRetry={config.kind === 'convex' ? check : undefined}\n />\n )\n }\n return <>{children}</>\n}\n\ntype GateState = { phase: 'checking' } | { phase: 'resolved'; report: DeploymentReadinessReport }\n\n/**\n * The checks that BLOCK boot: a failed deployment- or app-level precondition. The\n * per-user bucket (\"not a member\") is not a boot blocker — it is surfaced post-auth\n * (\"ask an admin\"), and at boot it is `unknown` (pre-auth) anyway.\n */\nfunction blockingFailures(report: DeploymentReadinessReport): ReadinessCheck[] {\n return report.checks.filter(\n (c) => c.status === 'fail' && (c.bucket === 'deployment' || c.bucket === 'app'),\n )\n}\n\n/** Deterministic boot state while the backend probe is in flight (also SSR output). */\nfunction DeploymentChecking() {\n return (\n <BootScreenFrame>\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.checking')}\n </Text>\n </BootScreenFrame>\n )\n}\n\n/**\n * The unified \"Deployment setup\" screen (EC-258): every failing check + its exact\n * fix. Pure/presentational so it is fixture-reproducible and static-render testable.\n */\nexport function DeploymentSetupScreen({\n report,\n onRetry,\n}: {\n report: DeploymentReadinessReport\n onRetry?: () => void\n}) {\n const failures = report.checks.filter((c) => c.status === 'fail')\n return (\n <BootScreenFrame>\n <Card className=\"max-w-2xl border-destructive/40\">\n <CardHeader>\n <CardTitle>{translate('shell.deploymentSetup.title')}</CardTitle>\n </CardHeader>\n <CardContent>\n <Stack gap=\"3\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.deploymentSetup.description')}\n </Text>\n <Stack gap=\"2\" role=\"list\" aria-label={translate('shell.deploymentSetup.issuesLabel')}>\n {failures.map((check) => (\n <CheckRow key={check.id} check={check} />\n ))}\n </Stack>\n {onRetry ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('shell.deploymentSetup.retry')}\n </Button>\n ) : null}\n </Stack>\n </CardContent>\n </Card>\n </BootScreenFrame>\n )\n}\n\nfunction CheckRow({ check }: { check: ReadinessCheck }) {\n return (\n <div role=\"listitem\" className=\"rounded-md border border-destructive/30 bg-destructive/5 p-2\">\n <Text size=\"xs\" mono className=\"text-destructive\">\n [{check.id}]\n </Text>\n <Text size=\"sm\">{check.detail}</Text>\n {check.fix ? (\n <Text size=\"xs\" tone=\"muted\" className=\"mt-1\">\n {translate('shell.deploymentSetup.fixLabel')}: <Code className=\"break-all\">{check.fix}</Code>\n </Text>\n ) : null}\n </div>\n )\n}\n\nfunction BootScreenFrame({ children }: { children: ReactNode }) {\n return <div className=\"grid min-h-dvh place-items-center bg-background p-6\">{children}</div>\n}\n"]}
|
|
@@ -7,7 +7,7 @@ import type { HydratedCms, HydratedProjectSession, OperationResult } from '@elyt
|
|
|
7
7
|
* builder from the latest persisted graph revision, the CMS from the saved
|
|
8
8
|
* schema + documents, publishing from the persisted environment states.
|
|
9
9
|
* This provider runs the `@elytracms/core/operations` hydration queries
|
|
10
|
-
* (`lib/operations/session-hydration`) and — like `
|
|
10
|
+
* (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its
|
|
11
11
|
* children on a deterministic loading state, so fixture content never
|
|
12
12
|
* flashes (and is never editable) before the persisted state arrives.
|
|
13
13
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `
|
|
1
|
+
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does. `enabled: true` below runs\n // this in the playground fast-path too (with `session: null`) — the one\n // divergence from `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(\n session?.cms ? { ...session.cms, routes: resolveDeploymentRoutes() } : null,\n )\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n if (playground) return { ok: true, value: null }\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n return result\n }, [playground, projectId])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
|
|
@@ -53,7 +53,7 @@ export function HydrationBoundary({ phase, retry, children, }) {
|
|
|
53
53
|
return _jsx(SessionHydrationError, { error: phase.error, onRetry: retry });
|
|
54
54
|
return _jsx(_Fragment, { children: children(phase.value) });
|
|
55
55
|
}
|
|
56
|
-
/** Deterministic loading frame while persisted state is fetched (mirrors
|
|
56
|
+
/** Deterministic loading frame while persisted state is fetched (mirrors DeploymentSetupGate). */
|
|
57
57
|
function SessionHydrating() {
|
|
58
58
|
return (_jsx("div", { className: "grid h-full min-h-64 flex-1 place-items-center p-8", "data-testid": "session-hydrating", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('shell.session.loading') }) }));
|
|
59
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-hydration-phase.js","sourceRoot":"","sources":["../../src/components/use-hydration-phase.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AA+CxD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAyC,EACzC,OAAoC;IAEpC,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IAChE,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAChD,aAAa,CAAC,OAAO,GAAG,OAAO,CAAC,UAAU,CAAA;IAE1C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAoB,GAAG,EAAE,CACzD,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAC7E,CAAA;IACD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEzC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,OAAO;gBAAE,aAAa,CAAC,OAAO,EAAE,CAAC,eAAe,CAAC,CAAA;YACrD,OAAM;QACR,CAAC;QACD,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAC7B,KAAK,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC5B,IAAI,SAAS;gBAAE,OAAM;YACrB,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;gBAChD,OAAM;YACR,CAAC;YACD,aAAa,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACrC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAA;QAClD,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAA;QAClB,CAAC,CAAA;QACD,qEAAqE;QACrE,yDAAyD;IAC3D,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAA;IAE3D,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAI,EACnC,KAAK,EACL,KAAK,EACL,QAAQ,GAKT;IACC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,KAAC,gBAAgB,KAAG,CAAA;IACzD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,KAAC,qBAAqB,IAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAI,CAAA;IAChG,OAAO,4BAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAI,CAAA;AACrC,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"use-hydration-phase.js","sourceRoot":"","sources":["../../src/components/use-hydration-phase.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Button, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { OperationErrorBanner } from './operation-error'\n\n/**\n * The shared hydration state machine (EC-329) behind both\n * `ProjectSessionProvider` and `CmsSessionGate`: playground short-circuit,\n * phase union, attempt-driven retry, cancelled-flag cleanup — a specific\n * multi-part shape the two providers used to hand-roll identically. Real\n * asymmetries between them stay explicit HOOK OPTIONS rather than being\n * flattened away:\n *\n * - `ProjectSessionProvider`'s playground branch calls its `onResolved` side\n * effect (`configureContentSession(null)`); `CmsSessionGate` passes no\n * `onResolved` at all (it owns no such side effect — `loadCms`'s own body\n * already calls it). `enabled` toggles whether `onResolved` fires in the\n * playground fast-path — the ONE point of divergence between the two.\n * - `onResolved` runs synchronously BEFORE the phase flips to `'ready'` (both\n * the playground fast-path and the real load), so children reading a\n * module global (e.g. `configureContentSession`) see it updated on the\n * SAME render they first see `'ready'` — a bare `loader().then(setPhase)`\n * cannot guarantee that ordering.\n */\nexport type HydrationPhase<T> =\n | { kind: 'loading' }\n | { kind: 'ready'; value: T }\n | { kind: 'error'; error: OperationError }\n\nexport interface UseHydrationPhaseOptions<T> {\n /** Playground (fixture) sessions resolve synchronously to `playgroundValue` — no loader call. */\n playground: boolean\n /** The value `phase` resolves to in playground mode. */\n playgroundValue: T\n /** Call `onResolved` for the playground fast-path too (default `false`). */\n enabled?: boolean\n /**\n * Run synchronously before the phase flips to `'ready'` — in BOTH the\n * playground fast-path (when `enabled`) and the real load. Read via a ref\n * internally, so an inline (non-memoized) callback never re-triggers a fetch.\n */\n onResolved?: (value: T) => void\n}\n\nexport interface UseHydrationPhaseResult<T> {\n phase: HydrationPhase<T>\n /** Re-run the loader (the retry button's handler). */\n retry: () => void\n}\n\n/**\n * `loader`'s IDENTITY is the refetch trigger — memoize it (`useCallback`) over\n * whatever inputs should re-hydrate (e.g. `projectId`), exactly like the\n * original two effects keyed their dependency arrays.\n */\nexport function useHydrationPhase<T>(\n loader: () => Promise<OperationResult<T>>,\n options: UseHydrationPhaseOptions<T>,\n): UseHydrationPhaseResult<T> {\n const { playground, playgroundValue, enabled = false } = options\n const onResolvedRef = useRef(options.onResolved)\n onResolvedRef.current = options.onResolved\n\n const [phase, setPhase] = useState<HydrationPhase<T>>(() =>\n playground ? { kind: 'ready', value: playgroundValue } : { kind: 'loading' },\n )\n const [attempt, setAttempt] = useState(0)\n\n useEffect(() => {\n if (playground) {\n if (enabled) onResolvedRef.current?.(playgroundValue)\n return\n }\n let cancelled = false\n setPhase({ kind: 'loading' })\n void loader().then((result) => {\n if (cancelled) return\n if (!result.ok) {\n setPhase({ kind: 'error', error: result.error })\n return\n }\n onResolvedRef.current?.(result.value)\n setPhase({ kind: 'ready', value: result.value })\n })\n return () => {\n cancelled = true\n }\n // `onResolved` is read via the ref above (see the doc comment) — its\n // identity intentionally does not retrigger this effect.\n }, [loader, playground, attempt, enabled, playgroundValue])\n\n const retry = useCallback(() => setAttempt((n) => n + 1), [])\n return { phase, retry }\n}\n\n/**\n * Renders `phase`'s loading/error frames, or `children(value)` once ready —\n * the identical 3-way branch both `ProjectSessionProvider` and\n * `CmsSessionGate` used to repeat verbatim.\n */\nexport function HydrationBoundary<T>({\n phase,\n retry,\n children,\n}: {\n phase: HydrationPhase<T>\n retry: () => void\n children: (value: T) => ReactNode\n}): ReactNode {\n if (phase.kind === 'loading') return <SessionHydrating />\n if (phase.kind === 'error') return <SessionHydrationError error={phase.error} onRetry={retry} />\n return <>{children(phase.value)}</>\n}\n\n/** Deterministic loading frame while persisted state is fetched (mirrors DeploymentSetupGate). */\nfunction SessionHydrating() {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\" data-testid=\"session-hydrating\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('shell.session.loading')}\n </Text>\n </div>\n )\n}\n\n/** A failed hydration is a visible, retryable state — never a fixture fallback. */\nfunction SessionHydrationError({\n error,\n onRetry,\n}: {\n error: OperationError\n onRetry: () => void\n}) {\n return (\n <div className=\"grid h-full min-h-64 flex-1 place-items-center p-8\">\n <Stack gap=\"3\" className=\"w-full max-w-xl\">\n <Text size=\"sm\" weight=\"semibold\">\n {translate('shell.session.error.title')}\n </Text>\n <OperationErrorBanner error={error} />\n <Text size=\"xs\" tone=\"muted\">\n {translate('shell.session.error.body')}\n </Text>\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry} className=\"self-start\">\n {translate('common.retry')}\n </Button>\n </Stack>\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Mail, Plus, X } from 'lucide-react';
|
|
4
|
+
import { roleOptions } from '@elytracms/core/persistence';
|
|
5
|
+
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Input, Row, Stack, Text, toast, } from '@elytracms/studio/ui';
|
|
6
|
+
import { getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
7
|
+
import { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query';
|
|
8
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
9
|
+
/**
|
|
10
|
+
* Invite-based onboarding UI (EC-324) — the admin "Invite User" action that
|
|
11
|
+
* REPLACES open sign-up, plus the list of pending invites. An admin invites an
|
|
12
|
+
* email + role; the invite is consumed when that email first creates their
|
|
13
|
+
* account. All three operations (`invites.create`/`list`/`revoke`) are admin-gated
|
|
14
|
+
* at the chokepoint AND server-side, so this UI is only mounted for admins (the
|
|
15
|
+
* Users module already gates non-admins).
|
|
16
|
+
*
|
|
17
|
+
* The invite form is a small ACTION dialog (email + role) — not a record property
|
|
18
|
+
* form — so a dialog is appropriate here (the house "no modal property forms" rule
|
|
19
|
+
* governs editing a record's fields, not a create action).
|
|
20
|
+
*/
|
|
21
|
+
const ROLE_OPTIONS = roleOptions();
|
|
22
|
+
const DEFAULT_INVITE_ROLE = ROLE_OPTIONS.find((r) => r.value === 'editor')?.value ?? ROLE_OPTIONS[0]?.value ?? 'viewer';
|
|
23
|
+
function invitesQueryKey(projectId) {
|
|
24
|
+
return ['invites', projectId];
|
|
25
|
+
}
|
|
26
|
+
export function InviteUserBar({ projectId }) {
|
|
27
|
+
const cache = useOperationCache();
|
|
28
|
+
const query = useOperationQuery({
|
|
29
|
+
queryKey: invitesQueryKey(projectId),
|
|
30
|
+
loader: async (operations) => {
|
|
31
|
+
const result = await operations.invites.list({ projectId });
|
|
32
|
+
if (!result.ok)
|
|
33
|
+
throw new Error(result.error.message);
|
|
34
|
+
return [...result.value];
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const { status } = listStoreState(query);
|
|
38
|
+
const invites = query.data ?? [];
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
async function revoke(email) {
|
|
41
|
+
const { operations } = await getStudioOperations();
|
|
42
|
+
const result = await operations.invites.revoke({ projectId, email });
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
toast.error(result.error.message);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
cache.patch(invitesQueryKey(projectId), (prev) => (prev ?? []).filter((invite) => invite.email !== email));
|
|
48
|
+
toast.success(translate('invites.revoked', { email }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsxs(Row, { gap: "3", align: "center", className: "shrink-0 flex-wrap border-b border-border px-4 py-2 sm:px-6", children: [_jsxs(Button, { size: "sm", onClick: () => setOpen(true), "data-testid": "invite-user-button", children: [_jsx(Plus, {}), translate('invites.invite')] }), status === 'ready' && invites.length > 0 ? (_jsxs(Row, { gap: "2", align: "center", className: "min-w-0 flex-wrap", children: [_jsx(Text, { size: "xs", tone: "muted", children: translate('invites.pending') }), invites.map((invite) => (_jsxs(Badge, { variant: "outline", className: "gap-1 font-normal", children: [_jsx(Mail, { className: "size-3" }), _jsx("span", { className: "truncate", children: invite.email }), _jsxs("span", { className: "text-muted-foreground", children: ["\u00B7 ", invite.role] }), _jsx("button", { type: "button", "aria-label": translate('invites.revokeAria', { email: invite.email }), onClick: () => void revoke(invite.email), className: "ml-0.5 rounded-sm text-muted-foreground hover:text-foreground", children: _jsx(X, { className: "size-3" }) })] }, invite.email)))] })) : null, _jsx(InviteDialog, { projectId: projectId, open: open, onOpenChange: setOpen, onInvited: (invite) => {
|
|
51
|
+
cache.patch(invitesQueryKey(projectId), (prev) => {
|
|
52
|
+
const rest = (prev ?? []).filter((i) => i.email !== invite.email);
|
|
53
|
+
return [...rest, invite].sort((a, b) => (a.email < b.email ? -1 : a.email > b.email ? 1 : 0));
|
|
54
|
+
});
|
|
55
|
+
} })] }));
|
|
56
|
+
}
|
|
57
|
+
function InviteDialog({ projectId, open, onOpenChange, onInvited, }) {
|
|
58
|
+
const [email, setEmail] = useState('');
|
|
59
|
+
const [role, setRole] = useState(DEFAULT_INVITE_ROLE);
|
|
60
|
+
const [phase, setPhase] = useState('idle');
|
|
61
|
+
const [error, setError] = useState(null);
|
|
62
|
+
function reset() {
|
|
63
|
+
setEmail('');
|
|
64
|
+
setRole(DEFAULT_INVITE_ROLE);
|
|
65
|
+
setError(null);
|
|
66
|
+
setPhase('idle');
|
|
67
|
+
}
|
|
68
|
+
async function handleSubmit(event) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
setError(null);
|
|
71
|
+
setPhase('submitting');
|
|
72
|
+
const { operations } = await getStudioOperations();
|
|
73
|
+
const result = await operations.invites.create({ projectId, email, role });
|
|
74
|
+
setPhase('idle');
|
|
75
|
+
if (!result.ok) {
|
|
76
|
+
setError(result.error.message);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
onInvited(result.value);
|
|
80
|
+
toast.success(translate('invites.sent', { email: result.value.email }));
|
|
81
|
+
reset();
|
|
82
|
+
onOpenChange(false);
|
|
83
|
+
}
|
|
84
|
+
const submitting = phase === 'submitting';
|
|
85
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (next) => {
|
|
86
|
+
if (!next)
|
|
87
|
+
reset();
|
|
88
|
+
onOpenChange(next);
|
|
89
|
+
}, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: translate('invites.dialogTitle') }), _jsx(DialogDescription, { children: translate('invites.dialogBody') })] }), _jsx("form", { onSubmit: handleSubmit, noValidate: true, children: _jsxs(Stack, { gap: "4", children: [_jsx(Field, { label: translate('invites.emailLabel'), htmlFor: "invite-email", required: true, error: error, children: _jsx(Input, { id: "invite-email", type: "email", autoComplete: "off", placeholder: "teammate@example.com", value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }) }), _jsx(Field, { label: translate('invites.roleLabel'), htmlFor: "invite-role", children: _jsx("select", { id: "invite-role", value: role, onChange: (e) => setRole(e.target.value), disabled: submitting, className: "h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", children: ROLE_OPTIONS.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), disabled: submitting, children: translate('common.cancel') }), _jsx(Button, { type: "submit", disabled: submitting || email.trim().length === 0, children: submitting ? translate('invites.sending') : translate('invites.send') })] })] }) })] }) }));
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=invite-user-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-user-bar.js","sourceRoot":"","sources":["../../../src/components/users/invite-user-bar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EACL,KAAK,EACL,MAAM,EACN,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,KAAK,EACL,GAAG,EACH,KAAK,EACL,IAAI,EACJ,KAAK,GACN,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAClG,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,YAAY,GAAG,WAAW,EAAE,CAAA;AAClC,MAAM,mBAAmB,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,EAAE,KAAK,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,QAAQ,CAAA;AAEvH,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAE,SAAS,EAAyB;IAChE,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAA;IACjC,MAAM,KAAK,GAAG,iBAAiB,CAAiB;QAC9C,QAAQ,EAAE,eAAe,CAAC,SAAS,CAAC;QACpC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;YAC3D,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACrD,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,CAAC;KACF,CAAC,CAAA;IACF,MAAM,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE,CAAA;IAEhC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEvC,KAAK,UAAU,MAAM,CAAC,KAAa;QACjC,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAClD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;QACpE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QACD,KAAK,CAAC,KAAK,CAAiB,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAC/D,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CACxD,CAAA;QACD,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IACxD,CAAC;IAED,OAAO,CACL,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,KAAK,EAAC,QAAQ,EACd,SAAS,EAAC,6DAA6D,aAEvE,MAAC,MAAM,IAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAc,oBAAoB,aAC9E,KAAC,IAAI,KAAG,EACP,SAAS,CAAC,gBAAgB,CAAC,IACrB,EAER,MAAM,KAAK,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAC1C,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,SAAS,EAAC,mBAAmB,aACvD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,iBAAiB,CAAC,GACxB,EACN,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CACvB,MAAC,KAAK,IAAoB,OAAO,EAAC,SAAS,EAAC,SAAS,EAAC,mBAAmB,aACvE,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,GAAG,EAC3B,eAAM,SAAS,EAAC,UAAU,YAAE,MAAM,CAAC,KAAK,GAAQ,EAChD,gBAAM,SAAS,EAAC,uBAAuB,wBAAI,MAAM,CAAC,IAAI,IAAQ,EAC9D,iBACE,IAAI,EAAC,QAAQ,gBACD,SAAS,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,EACpE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EACxC,SAAS,EAAC,+DAA+D,YAEzE,KAAC,CAAC,IAAC,SAAS,EAAC,QAAQ,GAAG,GACjB,KAXC,MAAM,CAAC,KAAK,CAYhB,CACT,CAAC,IACE,CACP,CAAC,CAAC,CAAC,IAAI,EAER,KAAC,YAAY,IACX,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,OAAO,EACrB,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE;oBACpB,KAAK,CAAC,KAAK,CAAiB,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;wBAC/D,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAA;wBACjE,OAAO,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;oBAC/F,CAAC,CAAC,CAAA;gBACJ,CAAC,GACD,IACE,CACP,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,SAAS,EACT,IAAI,EACJ,YAAY,EACZ,SAAS,GAMV;IACC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAS,mBAAmB,CAAC,CAAA;IAC7D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAwB,MAAM,CAAC,CAAA;IACjE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAEvD,SAAS,KAAK;QACZ,QAAQ,CAAC,EAAE,CAAC,CAAA;QACZ,OAAO,CAAC,mBAAmB,CAAC,CAAA;QAC5B,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClB,CAAC;IAED,KAAK,UAAU,YAAY,CAAC,KAAiC;QAC3D,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,QAAQ,CAAC,YAAY,CAAC,CAAA;QACtB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAA;QAClD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1E,QAAQ,CAAC,MAAM,CAAC,CAAA;QAChB,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAC9B,OAAM;QACR,CAAC;QACD,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACvB,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;QACvE,KAAK,EAAE,CAAA;QACP,YAAY,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,KAAK,YAAY,CAAA;IAEzC,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YACrB,IAAI,CAAC,IAAI;gBAAE,KAAK,EAAE,CAAA;YAClB,YAAY,CAAC,IAAI,CAAC,CAAA;QACpB,CAAC,YAED,MAAC,aAAa,eACZ,MAAC,YAAY,eACX,KAAC,WAAW,cAAE,SAAS,CAAC,qBAAqB,CAAC,GAAe,EAC7D,KAAC,iBAAiB,cAAE,SAAS,CAAC,oBAAoB,CAAC,GAAqB,IAC3D,EACf,eAAM,QAAQ,EAAE,YAAY,EAAE,UAAU,kBACtC,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAC,cAAc,EAAC,QAAQ,QAAC,KAAK,EAAE,KAAK,YACzF,KAAC,KAAK,IACJ,EAAE,EAAC,cAAc,EACjB,IAAI,EAAC,OAAO,EACZ,YAAY,EAAC,KAAK,EAClB,WAAW,EAAC,sBAAsB,EAClC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,QAAQ,EAAE,UAAU,GACpB,GACI,EACR,KAAC,KAAK,IAAC,KAAK,EAAE,SAAS,CAAC,mBAAmB,CAAC,EAAE,OAAO,EAAC,aAAa,YACjE,iBACE,EAAE,EAAC,aAAa,EAChB,KAAK,EAAE,IAAI,EACX,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACxC,QAAQ,EAAE,UAAU,EACpB,SAAS,EAAC,wJAAwJ,YAEjK,YAAY,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAC5B,iBAA2B,KAAK,EAAE,MAAM,CAAC,KAAK,YAC3C,MAAM,CAAC,KAAK,IADF,MAAM,CAAC,KAAK,CAEhB,CACV,CAAC,GACK,GACH,EACR,MAAC,YAAY,eACX,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,UAAU,YAC3F,SAAS,CAAC,eAAe,CAAC,GACpB,EACT,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,UAAU,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,YACpE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,GAC/D,IACI,IACT,GACH,IACO,GACT,CACV,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent } from 'react'\nimport { Mail, Plus, X } from 'lucide-react'\nimport type { InviteRecord } from '@elytracms/core/persistence'\nimport { roleOptions } from '@elytracms/core/persistence'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Row,\n Stack,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Invite-based onboarding UI (EC-324) — the admin \"Invite User\" action that\n * REPLACES open sign-up, plus the list of pending invites. An admin invites an\n * email + role; the invite is consumed when that email first creates their\n * account. All three operations (`invites.create`/`list`/`revoke`) are admin-gated\n * at the chokepoint AND server-side, so this UI is only mounted for admins (the\n * Users module already gates non-admins).\n *\n * The invite form is a small ACTION dialog (email + role) — not a record property\n * form — so a dialog is appropriate here (the house \"no modal property forms\" rule\n * governs editing a record's fields, not a create action).\n */\nconst ROLE_OPTIONS = roleOptions()\nconst DEFAULT_INVITE_ROLE = ROLE_OPTIONS.find((r) => r.value === 'editor')?.value ?? ROLE_OPTIONS[0]?.value ?? 'viewer'\n\nfunction invitesQueryKey(projectId: string): readonly [string, string] {\n return ['invites', projectId]\n}\n\nexport function InviteUserBar({ projectId }: { projectId: string }) {\n const cache = useOperationCache()\n const query = useOperationQuery<InviteRecord[]>({\n queryKey: invitesQueryKey(projectId),\n loader: async (operations) => {\n const result = await operations.invites.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n },\n })\n const { status } = listStoreState(query)\n const invites = query.data ?? []\n\n const [open, setOpen] = useState(false)\n\n async function revoke(email: string) {\n const { operations } = await getStudioOperations()\n const result = await operations.invites.revoke({ projectId, email })\n if (!result.ok) {\n toast.error(result.error.message)\n return\n }\n cache.patch<InviteRecord[]>(invitesQueryKey(projectId), (prev) =>\n (prev ?? []).filter((invite) => invite.email !== email),\n )\n toast.success(translate('invites.revoked', { email }))\n }\n\n return (\n <Row\n gap=\"3\"\n align=\"center\"\n className=\"shrink-0 flex-wrap border-b border-border px-4 py-2 sm:px-6\"\n >\n <Button size=\"sm\" onClick={() => setOpen(true)} data-testid=\"invite-user-button\">\n <Plus />\n {translate('invites.invite')}\n </Button>\n\n {status === 'ready' && invites.length > 0 ? (\n <Row gap=\"2\" align=\"center\" className=\"min-w-0 flex-wrap\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('invites.pending')}\n </Text>\n {invites.map((invite) => (\n <Badge key={invite.email} variant=\"outline\" className=\"gap-1 font-normal\">\n <Mail className=\"size-3\" />\n <span className=\"truncate\">{invite.email}</span>\n <span className=\"text-muted-foreground\">· {invite.role}</span>\n <button\n type=\"button\"\n aria-label={translate('invites.revokeAria', { email: invite.email })}\n onClick={() => void revoke(invite.email)}\n className=\"ml-0.5 rounded-sm text-muted-foreground hover:text-foreground\"\n >\n <X className=\"size-3\" />\n </button>\n </Badge>\n ))}\n </Row>\n ) : null}\n\n <InviteDialog\n projectId={projectId}\n open={open}\n onOpenChange={setOpen}\n onInvited={(invite) => {\n cache.patch<InviteRecord[]>(invitesQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((i) => i.email !== invite.email)\n return [...rest, invite].sort((a, b) => (a.email < b.email ? -1 : a.email > b.email ? 1 : 0))\n })\n }}\n />\n </Row>\n )\n}\n\nfunction InviteDialog({\n projectId,\n open,\n onOpenChange,\n onInvited,\n}: {\n projectId: string\n open: boolean\n onOpenChange: (open: boolean) => void\n onInvited: (invite: InviteRecord) => void\n}) {\n const [email, setEmail] = useState('')\n const [role, setRole] = useState<string>(DEFAULT_INVITE_ROLE)\n const [phase, setPhase] = useState<'idle' | 'submitting'>('idle')\n const [error, setError] = useState<string | null>(null)\n\n function reset() {\n setEmail('')\n setRole(DEFAULT_INVITE_ROLE)\n setError(null)\n setPhase('idle')\n }\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setError(null)\n setPhase('submitting')\n const { operations } = await getStudioOperations()\n const result = await operations.invites.create({ projectId, email, role })\n setPhase('idle')\n if (!result.ok) {\n setError(result.error.message)\n return\n }\n onInvited(result.value)\n toast.success(translate('invites.sent', { email: result.value.email }))\n reset()\n onOpenChange(false)\n }\n\n const submitting = phase === 'submitting'\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) reset()\n onOpenChange(next)\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('invites.dialogTitle')}</DialogTitle>\n <DialogDescription>{translate('invites.dialogBody')}</DialogDescription>\n </DialogHeader>\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"4\">\n <Field label={translate('invites.emailLabel')} htmlFor=\"invite-email\" required error={error}>\n <Input\n id=\"invite-email\"\n type=\"email\"\n autoComplete=\"off\"\n placeholder=\"teammate@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Field label={translate('invites.roleLabel')} htmlFor=\"invite-role\">\n <select\n id=\"invite-role\"\n value={role}\n onChange={(e) => setRole(e.target.value)}\n disabled={submitting}\n className=\"h-9 w-full rounded-md border border-input bg-background px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n >\n {ROLE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </Field>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={() => onOpenChange(false)} disabled={submitting}>\n {translate('common.cancel')}\n </Button>\n <Button type=\"submit\" disabled={submitting || email.trim().length === 0}>\n {submitting ? translate('invites.sending') : translate('invites.send')}\n </Button>\n </DialogFooter>\n </Stack>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -2,13 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ShieldAlert } from 'lucide-react';
|
|
3
3
|
import { Heading, Stack, Text } from '@elytracms/studio/ui';
|
|
4
4
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
-
import { useProjectRole } from '../auth-context';
|
|
5
|
+
import { useAuth, useProjectRole } from '../auth-context';
|
|
6
6
|
import { UsersProvider, useUsers } from './users-store';
|
|
7
7
|
import { useUsersCollectionViewSource } from './users-collection-view-source';
|
|
8
8
|
import { CollectionViewProvider } from '../collection-view/collection-view-context';
|
|
9
9
|
import { RailStoreStatus } from '../rail-store-status';
|
|
10
10
|
import { LoadedCollectionTable } from '../cms/collection-table';
|
|
11
11
|
import { PortableDetailEditor } from '../cms/detail-editor';
|
|
12
|
+
import { AccountSettings } from '../account/account-settings';
|
|
13
|
+
import { InviteUserBar } from './invite-user-bar';
|
|
12
14
|
/**
|
|
13
15
|
* The studio Users module (EC-240). An admin-only section for managing who has
|
|
14
16
|
* access to this deployment and their role. AD-19: it is the **reuse proof** for
|
|
@@ -29,7 +31,16 @@ import { PortableDetailEditor } from '../cms/detail-editor';
|
|
|
29
31
|
* authorization error, and we skip firing a query we know will be denied.
|
|
30
32
|
*/
|
|
31
33
|
export function UsersModule({ projectId, userId = null, }) {
|
|
34
|
+
const auth = useAuth();
|
|
32
35
|
const role = useProjectRole(projectId);
|
|
36
|
+
const currentUserId = auth.session?.user?.id ?? null;
|
|
37
|
+
// Self-access (EC-240 H-tail item 1): ANY member — admin or not — may open their
|
|
38
|
+
// OWN record, and only that. This is the account-settings surface (self-permitted
|
|
39
|
+
// read + identity/credential writes), routed here from the user menu. A non-admin
|
|
40
|
+
// opening someone ELSE's `users/<id>` still falls through to the restricted state.
|
|
41
|
+
if (userId !== null && currentUserId !== null && userId === currentUserId) {
|
|
42
|
+
return _jsx(AccountSettings, { projectId: projectId });
|
|
43
|
+
}
|
|
33
44
|
// `useProjectRole` conflates "loading" and "not a member" as null; in the studio
|
|
34
45
|
// a member always reaches here, so null resolves to the real role momentarily.
|
|
35
46
|
if (role === null) {
|
|
@@ -55,6 +66,6 @@ function UsersSurface({ projectId, userId }) {
|
|
|
55
66
|
if (userId !== null) {
|
|
56
67
|
return _jsx(PortableDetailEditor, { source: source, projectId: projectId, documentId: userId });
|
|
57
68
|
}
|
|
58
|
-
return (_jsx(CollectionViewProvider, { source: source, children: _jsx(LoadedCollectionTable, { projectId: projectId }) }));
|
|
69
|
+
return (_jsxs(Stack, { gap: "none", className: "h-full min-h-0", children: [_jsx(InviteUserBar, { projectId: projectId }), _jsx("div", { className: "min-h-0 flex-1", children: _jsx(CollectionViewProvider, { source: source, children: _jsx(LoadedCollectionTable, { projectId: projectId }) }) })] }));
|
|
59
70
|
}
|
|
60
71
|
//# sourceMappingURL=users-module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users-module.js","sourceRoot":"","sources":["../../../src/components/users/users-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"users-module.js","sourceRoot":"","sources":["../../../src/components/users/users-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,MAAM,GAAG,IAAI,GAId;IACC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAA;IACtB,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IACtC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI,CAAA;IAEpD,iFAAiF;IACjF,kFAAkF;IAClF,kFAAkF;IAClF,mFAAmF;IACnF,IAAI,MAAM,KAAK,IAAI,IAAI,aAAa,KAAK,IAAI,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC;QAC1E,OAAO,KAAC,eAAe,IAAC,SAAS,EAAE,SAAS,GAAI,CAAA;IAClD,CAAC;IAED,iFAAiF;IACjF,+EAA+E;IAC/E,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,CACL,KAAC,KAAK,IAAC,SAAS,EAAC,+BAA+B,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,YAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,sBAAsB,CAAC,GAC7B,GACD,CACT,CAAA;IACH,CAAC;IACD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,KAAC,eAAe,KAAG,CAAA;IAEhD,OAAO,CACL,KAAC,aAAa,IAAC,SAAS,EAAE,SAAS,YACjC,KAAC,YAAY,IAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAI,GACxC,CACjB,CAAA;AACH,CAAC;AAED,iEAAiE;AACjE,SAAS,eAAe;IACtB,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,EAAC,SAAS,EAAC,wBAAwB,aAC/E,KAAC,WAAW,IAAC,SAAS,EAAC,8BAA8B,GAAG,EACxD,KAAC,OAAO,IAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAC,IAAI,YACzB,SAAS,CAAC,uBAAuB,CAAC,GAC3B,EACV,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,UAAU,YAC9C,SAAS,CAAC,sBAAsB,CAAC,GAC7B,IACD,CACT,CAAA;AACH,CAAC;AAED,0FAA0F;AAC1F,SAAS,YAAY,CAAC,EAAE,SAAS,EAAE,MAAM,EAAgD;IACvF,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IACxB,MAAM,MAAM,GAAG,4BAA4B,CAAC,SAAS,CAAC,CAAA;IAEtD,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,KAAC,eAAe,IACd,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,SAAS,CAAC,kBAAkB,CAAC,EACzC,YAAY,EAAE,SAAS,CAAC,eAAe,CAAC,EACxC,WAAW,EAAC,aAAa,GACzB,CACH,CAAA;IACH,CAAC;IAED,kFAAkF;IAClF,gFAAgF;IAChF,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,KAAC,oBAAoB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAI,CAAA;IAC3F,CAAC;IAED,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,gBAAgB,aAE1C,KAAC,aAAa,IAAC,SAAS,EAAE,SAAS,GAAI,EACvC,cAAK,SAAS,EAAC,gBAAgB,YAC7B,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,qBAAqB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxB,GACrB,IACA,CACT,CAAA;AACH,CAAC","sourcesContent":["import { ShieldAlert } from 'lucide-react'\nimport { Heading, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useAuth, useProjectRole } from '../auth-context'\nimport { UsersProvider, useUsers } from './users-store'\nimport { useUsersCollectionViewSource } from './users-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { RailStoreStatus } from '../rail-store-status'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\nimport { AccountSettings } from '../account/account-settings'\nimport { InviteUserBar } from './invite-user-bar'\n\n/**\n * The studio Users module (EC-240). An admin-only section for managing who has\n * access to this deployment and their role. AD-19: it is the **reuse proof** for\n * the portable interaction surfaces — it renders the SAME `LoadedCollectionTable`\n * the CMS uses for the roster, and the SAME `DetailEditor` body (via\n * {@link PortableDetailEditor}) for a member's detail, both over a users\n * {@link useUsersCollectionViewSource} instead of a CMS source. No bespoke roster,\n * no bespoke editor: filtering, columns, the count, and the edit form all come\n * from the shared surfaces. The users capability profile (admin-only, no publish/\n * versions/create/import) drives which regions appear.\n *\n * `userId === null` is the roster; a `userId` opens that member's detail (routed\n * via `studioPath.user`). Both render under one `UsersProvider` + one source.\n *\n * Gating is layered: the operations chokepoint AND the Convex handlers enforce\n * admin-only server-side (a non-admin `users.list` is denied). This UI gate is UX\n * politeness — a non-admin sees a clear \"restricted\" state instead of a raw\n * authorization error, and we skip firing a query we know will be denied.\n */\nexport function UsersModule({\n projectId,\n userId = null,\n}: {\n projectId: string\n userId?: string | null\n}) {\n const auth = useAuth()\n const role = useProjectRole(projectId)\n const currentUserId = auth.session?.user?.id ?? null\n\n // Self-access (EC-240 H-tail item 1): ANY member — admin or not — may open their\n // OWN record, and only that. This is the account-settings surface (self-permitted\n // read + identity/credential writes), routed here from the user menu. A non-admin\n // opening someone ELSE's `users/<id>` still falls through to the restricted state.\n if (userId !== null && currentUserId !== null && userId === currentUserId) {\n return <AccountSettings projectId={projectId} />\n }\n\n // `useProjectRole` conflates \"loading\" and \"not a member\" as null; in the studio\n // a member always reaches here, so null resolves to the real role momentarily.\n if (role === null) {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('users.checkingAccess')}\n </Text>\n </Stack>\n )\n }\n if (role !== 'admin') return <RestrictedState />\n\n return (\n <UsersProvider projectId={projectId}>\n <UsersSurface projectId={projectId} userId={userId} />\n </UsersProvider>\n )\n}\n\n/** The non-admin gate: honest about why the surface is empty. */\nfunction RestrictedState() {\n return (\n <Stack gap=\"2\" align=\"center\" justify=\"center\" className=\"h-full p-8 text-center\">\n <ShieldAlert className=\"size-6 text-muted-foreground\" />\n <Heading level={2} size=\"h3\">\n {translate('users.restrictedTitle')}\n </Heading>\n <Text size=\"sm\" tone=\"muted\" className=\"max-w-sm\">\n {translate('users.restrictedBody')}\n </Text>\n </Stack>\n )\n}\n\n/** The loaded surface: the portable list (or a member's detail) over the users source. */\nfunction UsersSurface({ projectId, userId }: { projectId: string; userId: string | null }) {\n const store = useUsers()\n const source = useUsersCollectionViewSource(projectId)\n\n if (store.status !== 'ready') {\n return (\n <RailStoreStatus\n store={store}\n errorLabel={translate('users.loadFailed')}\n loadingLabel={translate('users.loading')}\n errorTestId=\"users-error\"\n />\n )\n }\n\n // A member's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (userId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={userId} />\n }\n\n return (\n <Stack gap=\"none\" className=\"h-full min-h-0\">\n {/* EC-324: invite-based onboarding — the admin action that replaces open sign-up. */}\n <InviteUserBar projectId={projectId} />\n <div className=\"min-h-0 flex-1\">\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n </div>\n </Stack>\n )\n}\n"]}
|
|
@@ -8,15 +8,23 @@
|
|
|
8
8
|
* monotonic counter in state, so a scripted action sequence yields identical output
|
|
9
9
|
* every run (determinism principle).
|
|
10
10
|
*/
|
|
11
|
+
import type { FieldValues } from '@elytracms/core/cms-core';
|
|
11
12
|
import type { AiProvenance, ContentPatch } from './types';
|
|
12
|
-
import type { CreateFlowResult, CreateFlowStep } from './create-flow';
|
|
13
|
+
import type { CreateFlowResult, CreateFlowStep, ProposalOrigin } from './create-flow';
|
|
13
14
|
/** A proposal the editor reviews as a diff and then applies (as a draft) or rejects. */
|
|
14
15
|
export interface ProposalReview {
|
|
15
16
|
id: string;
|
|
17
|
+
/** Fresh authoring ('create') vs a fill-empty translation ('translate') of an existing doc. */
|
|
18
|
+
origin: ProposalOrigin;
|
|
16
19
|
collection: string;
|
|
17
20
|
documentId: string;
|
|
18
21
|
patch: ContentPatch;
|
|
19
22
|
provenance: AiProvenance;
|
|
23
|
+
/** For a translation: the existing values the diff renders "before". */
|
|
24
|
+
before?: {
|
|
25
|
+
values?: FieldValues;
|
|
26
|
+
localized?: Record<string, FieldValues>;
|
|
27
|
+
};
|
|
20
28
|
status: 'pending' | 'applied' | 'rejected';
|
|
21
29
|
}
|
|
22
30
|
/** One rendered turn in the conversation. */
|
|
@@ -48,10 +48,12 @@ export function chatReducer(state, action) {
|
|
|
48
48
|
if (action.result.kind === 'proposal') {
|
|
49
49
|
const proposal = {
|
|
50
50
|
id: msgId,
|
|
51
|
+
origin: action.result.origin,
|
|
51
52
|
collection: action.result.collection,
|
|
52
53
|
documentId: action.result.documentId,
|
|
53
54
|
patch: action.result.patch,
|
|
54
55
|
provenance: action.result.provenance,
|
|
56
|
+
...(action.result.before ? { before: action.result.before } : {}),
|
|
55
57
|
status: 'pending',
|
|
56
58
|
};
|
|
57
59
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-state.js","sourceRoot":"","sources":["../../../src/lib/ai/chat-state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The chat panel's pure state core (EC-199).\n *\n * House testing pattern (AGENTS.md): no jsdom / renderHook — the panel's behaviour\n * lives in this deterministic reducer (submit → busy + activity, tool steps append,\n * a result becomes a message or a reviewable proposal, apply/reject transition the\n * proposal), tested directly; the React component is a thin shell over it. Ids are a\n * monotonic counter in state, so a scripted action sequence yields identical output\n * every run (determinism principle).\n */\nimport type { AiProvenance, ContentPatch } from './types'\nimport type { CreateFlowResult, CreateFlowStep } from './create-flow'\n\n/** A proposal the editor reviews as a diff and then applies (as a draft) or rejects. */\nexport interface ProposalReview {\n id: string\n collection: string\n documentId: string\n patch: ContentPatch\n provenance: AiProvenance\n status: 'pending' | 'applied' | 'rejected'\n}\n\n/** One rendered turn in the conversation. */\nexport type ChatMessage =\n | { id: string; role: 'user'; text: string }\n | { id: string; role: 'assistant'; kind: 'text'; text: string }\n | { id: string; role: 'assistant'; kind: 'activity'; steps: CreateFlowStep[] }\n | { id: string; role: 'assistant'; kind: 'proposal'; proposal: ProposalReview }\n\nexport interface ChatState {\n status: 'idle' | 'busy'\n messages: ChatMessage[]\n /** Id of the live activity message the current turn appends tool steps to. */\n activityId: string | null\n /** Last blocking error (route/provider failure), surfaced then cleared on next submit. */\n error: string | null\n /** Monotonic id source — keeps the reducer pure + deterministic. */\n seq: number\n}\n\nexport const initialChatState: ChatState = {\n status: 'idle',\n messages: [],\n activityId: null,\n error: null,\n seq: 0,\n}\n\nexport type ChatAction =\n | { type: 'submit'; text: string }\n | { type: 'step'; step: CreateFlowStep }\n | { type: 'result'; result: CreateFlowResult }\n | { type: 'fail'; message: string }\n | { type: 'apply'; proposalId: string }\n | { type: 'reject'; proposalId: string }\n\nfunction id(state: ChatState): [string, number] {\n return [`m${state.seq}`, state.seq + 1]\n}\n\n/** Drop a still-empty activity message (a turn that produced no tool steps). */\nfunction pruneEmptyActivity(messages: ChatMessage[], activityId: string | null): ChatMessage[] {\n if (!activityId) return messages\n return messages.filter(\n (m) =>\n !(\n m.id === activityId &&\n m.role === 'assistant' &&\n m.kind === 'activity' &&\n m.steps.length === 0\n ),\n )\n}\n\nexport function chatReducer(state: ChatState, action: ChatAction): ChatState {\n switch (action.type) {\n case 'submit': {\n const text = action.text.trim()\n if (text.length === 0 || state.status === 'busy') return state\n const [userId, s1] = id(state)\n const activityId = `m${s1}`\n const messages: ChatMessage[] = [\n ...state.messages,\n { id: userId, role: 'user', text },\n { id: activityId, role: 'assistant', kind: 'activity', steps: [] },\n ]\n return { ...state, status: 'busy', error: null, messages, activityId, seq: s1 + 1 }\n }\n case 'step': {\n if (!state.activityId) return state\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.id === state.activityId && m.role === 'assistant' && m.kind === 'activity'\n ? { ...m, steps: [...m.steps, action.step] }\n : m,\n ),\n }\n }\n case 'result': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id({ ...state, seq: state.seq })\n if (action.result.kind === 'proposal') {\n const proposal: ProposalReview = {\n id: msgId,\n collection: action.result.collection,\n documentId: action.result.documentId,\n patch: action.result.patch,\n provenance: action.result.provenance,\n status: 'pending',\n }\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'proposal', proposal }],\n }\n }\n const text =\n action.result.kind === 'message' ? action.result.text : action.result.message\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n error: action.result.kind === 'error' ? text : null,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text }],\n }\n }\n case 'fail': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id(state)\n return {\n ...state,\n status: 'idle',\n activityId: null,\n error: action.message,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text: action.message }],\n }\n }\n case 'apply':\n case 'reject': {\n const next = action.type === 'apply' ? 'applied' : 'rejected'\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.role === 'assistant' && m.kind === 'proposal' && m.proposal.id === action.proposalId\n ? { ...m, proposal: { ...m.proposal, status: next } }\n : m,\n ),\n }\n }\n default:\n return state\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"chat-state.js","sourceRoot":"","sources":["../../../src/lib/ai/chat-state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The chat panel's pure state core (EC-199).\n *\n * House testing pattern (AGENTS.md): no jsdom / renderHook — the panel's behaviour\n * lives in this deterministic reducer (submit → busy + activity, tool steps append,\n * a result becomes a message or a reviewable proposal, apply/reject transition the\n * proposal), tested directly; the React component is a thin shell over it. Ids are a\n * monotonic counter in state, so a scripted action sequence yields identical output\n * every run (determinism principle).\n */\nimport type { FieldValues } from '@elytracms/core/cms-core'\nimport type { AiProvenance, ContentPatch } from './types'\nimport type { CreateFlowResult, CreateFlowStep, ProposalOrigin } from './create-flow'\n\n/** A proposal the editor reviews as a diff and then applies (as a draft) or rejects. */\nexport interface ProposalReview {\n id: string\n /** Fresh authoring ('create') vs a fill-empty translation ('translate') of an existing doc. */\n origin: ProposalOrigin\n collection: string\n documentId: string\n patch: ContentPatch\n provenance: AiProvenance\n /** For a translation: the existing values the diff renders \"before\". */\n before?: { values?: FieldValues; localized?: Record<string, FieldValues> }\n status: 'pending' | 'applied' | 'rejected'\n}\n\n/** One rendered turn in the conversation. */\nexport type ChatMessage =\n | { id: string; role: 'user'; text: string }\n | { id: string; role: 'assistant'; kind: 'text'; text: string }\n | { id: string; role: 'assistant'; kind: 'activity'; steps: CreateFlowStep[] }\n | { id: string; role: 'assistant'; kind: 'proposal'; proposal: ProposalReview }\n\nexport interface ChatState {\n status: 'idle' | 'busy'\n messages: ChatMessage[]\n /** Id of the live activity message the current turn appends tool steps to. */\n activityId: string | null\n /** Last blocking error (route/provider failure), surfaced then cleared on next submit. */\n error: string | null\n /** Monotonic id source — keeps the reducer pure + deterministic. */\n seq: number\n}\n\nexport const initialChatState: ChatState = {\n status: 'idle',\n messages: [],\n activityId: null,\n error: null,\n seq: 0,\n}\n\nexport type ChatAction =\n | { type: 'submit'; text: string }\n | { type: 'step'; step: CreateFlowStep }\n | { type: 'result'; result: CreateFlowResult }\n | { type: 'fail'; message: string }\n | { type: 'apply'; proposalId: string }\n | { type: 'reject'; proposalId: string }\n\nfunction id(state: ChatState): [string, number] {\n return [`m${state.seq}`, state.seq + 1]\n}\n\n/** Drop a still-empty activity message (a turn that produced no tool steps). */\nfunction pruneEmptyActivity(messages: ChatMessage[], activityId: string | null): ChatMessage[] {\n if (!activityId) return messages\n return messages.filter(\n (m) =>\n !(\n m.id === activityId &&\n m.role === 'assistant' &&\n m.kind === 'activity' &&\n m.steps.length === 0\n ),\n )\n}\n\nexport function chatReducer(state: ChatState, action: ChatAction): ChatState {\n switch (action.type) {\n case 'submit': {\n const text = action.text.trim()\n if (text.length === 0 || state.status === 'busy') return state\n const [userId, s1] = id(state)\n const activityId = `m${s1}`\n const messages: ChatMessage[] = [\n ...state.messages,\n { id: userId, role: 'user', text },\n { id: activityId, role: 'assistant', kind: 'activity', steps: [] },\n ]\n return { ...state, status: 'busy', error: null, messages, activityId, seq: s1 + 1 }\n }\n case 'step': {\n if (!state.activityId) return state\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.id === state.activityId && m.role === 'assistant' && m.kind === 'activity'\n ? { ...m, steps: [...m.steps, action.step] }\n : m,\n ),\n }\n }\n case 'result': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id({ ...state, seq: state.seq })\n if (action.result.kind === 'proposal') {\n const proposal: ProposalReview = {\n id: msgId,\n origin: action.result.origin,\n collection: action.result.collection,\n documentId: action.result.documentId,\n patch: action.result.patch,\n provenance: action.result.provenance,\n ...(action.result.before ? { before: action.result.before } : {}),\n status: 'pending',\n }\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'proposal', proposal }],\n }\n }\n const text =\n action.result.kind === 'message' ? action.result.text : action.result.message\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n error: action.result.kind === 'error' ? text : null,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text }],\n }\n }\n case 'fail': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id(state)\n return {\n ...state,\n status: 'idle',\n activityId: null,\n error: action.message,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text: action.message }],\n }\n }\n case 'apply':\n case 'reject': {\n const next = action.type === 'apply' ? 'applied' : 'rejected'\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.role === 'assistant' && m.kind === 'proposal' && m.proposal.id === action.proposalId\n ? { ...m, proposal: { ...m.proposal, status: next } }\n : m,\n ),\n }\n }\n default:\n return state\n }\n}\n"]}
|