@elytracms/studio 0.0.80 → 0.0.81
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/contract-parity.test.ts +1 -1
- package/convex/functions.ts +13 -3
- package/convex/http-routes.test.ts +143 -0
- package/convex/http.ts +19 -9
- package/convex/schema.ts +0 -19
- package/convex/users.ts +1 -1
- package/dist/components/app-views/default-modules.d.ts +141 -44
- package/dist/components/app-views/default-modules.js +229 -65
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +38 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +5 -2
- package/dist/components/app-views/sidebar.js +16 -5
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +27 -0
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +7 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse-context.js +7 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +94 -16
- package/dist/components/builder/reuse.js +201 -19
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +3 -1
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.js +6 -2
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +40 -13
- package/dist/components/cms/cms-store.js +50 -22
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +2 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +11 -5
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/field-control.js +2 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.js +8 -1
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/import-export.js +2 -1
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issues-pane.d.ts +4 -2
- package/dist/components/cms/issues-pane.js +6 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +10 -12
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/project-list.d.ts +9 -3
- package/dist/components/project-list.js +11 -5
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-switcher.d.ts +3 -1
- package/dist/components/project-switcher.js +4 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/redirects/redirects-store.d.ts +21 -0
- package/dist/components/redirects/redirects-store.js +17 -4
- package/dist/components/redirects/redirects-store.js.map +1 -1
- package/dist/components/section-nav.js +102 -4
- package/dist/components/section-nav.js.map +1 -1
- package/dist/customization.d.ts +7 -0
- package/dist/customization.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +6 -1
- package/dist/lib/data/collection-content.js +50 -12
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +132 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +103 -0
- package/dist/lib/i18n/messages.en.js +146 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/locale/binding-fallback.d.ts +8 -1
- package/dist/lib/locale/binding-fallback.js +8 -1
- package/dist/lib/locale/binding-fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.d.ts +41 -9
- package/dist/lib/locale/locale-store.js +34 -15
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/locale/locale-switcher.d.ts +3 -1
- package/dist/lib/locale/locale-switcher.js +4 -2
- package/dist/lib/locale/locale-switcher.js.map +1 -1
- package/dist/lib/locale/use-studio-locale.d.ts +9 -4
- package/dist/lib/locale/use-studio-locale.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +1 -3
- package/dist/lib/persistence/convex-adapter.js +0 -55
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +42 -1
- package/dist/lib/persona/labels.js +57 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/routes/route-issues.d.ts +9 -0
- package/dist/lib/routes/route-issues.js +117 -0
- package/dist/lib/routes/route-issues.js.map +1 -0
- package/dist/lib/workspace/config-issues.d.ts +54 -0
- package/dist/lib/workspace/config-issues.js +70 -0
- package/dist/lib/workspace/config-issues.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +9 -0
- package/dist/lib/workspace/index.js +11 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.d.ts +20 -3
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/package.json +2 -2
- package/convex/cliTokens.ts +0 -63
- package/convex/sync.ts +0 -297
|
@@ -8,8 +8,14 @@ export interface ProjectLauncherProps {
|
|
|
8
8
|
* Studio's front door: lists the workspace's projects and routes into a
|
|
9
9
|
* project's shell. With a workspace config the list is config-driven
|
|
10
10
|
* (configured projects ∩ grants, plus explicit "not provisioned" states);
|
|
11
|
-
* project *creation* is code-first — `elytra.config.ts
|
|
12
|
-
*
|
|
13
|
-
* adapter-backed list (and the starter-kit gallery)
|
|
11
|
+
* project *creation* is code-first — declare it in `elytra.config.ts`, then
|
|
12
|
+
* provision the backend with `elytra init` — so there is no in-UI create flow.
|
|
13
|
+
* Without a config the adapter-backed list (and the starter-kit gallery)
|
|
14
|
+
* behaves as before.
|
|
15
|
+
*
|
|
16
|
+
* EC-470: this used to instruct `builder init --project <slug>`. No `builder`
|
|
17
|
+
* bin was ever declared, so that was an unrunnable command shown as product UI.
|
|
18
|
+
* The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`
|
|
19
|
+
* reports what is still missing).
|
|
14
20
|
*/
|
|
15
21
|
export declare function ProjectLauncher({ forcedState }: ProjectLauncherProps): import("react").JSX.Element;
|
|
@@ -15,9 +15,15 @@ import { useRecentProjects, useSwitcherEntries } from './workspace-projects.js';
|
|
|
15
15
|
* Studio's front door: lists the workspace's projects and routes into a
|
|
16
16
|
* project's shell. With a workspace config the list is config-driven
|
|
17
17
|
* (configured projects ∩ grants, plus explicit "not provisioned" states);
|
|
18
|
-
* project *creation* is code-first — `elytra.config.ts
|
|
19
|
-
*
|
|
20
|
-
* adapter-backed list (and the starter-kit gallery)
|
|
18
|
+
* project *creation* is code-first — declare it in `elytra.config.ts`, then
|
|
19
|
+
* provision the backend with `elytra init` — so there is no in-UI create flow.
|
|
20
|
+
* Without a config the adapter-backed list (and the starter-kit gallery)
|
|
21
|
+
* behaves as before.
|
|
22
|
+
*
|
|
23
|
+
* EC-470: this used to instruct `builder init --project <slug>`. No `builder`
|
|
24
|
+
* bin was ever declared, so that was an unrunnable command shown as product UI.
|
|
25
|
+
* The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`
|
|
26
|
+
* reports what is still missing).
|
|
21
27
|
*/
|
|
22
28
|
export function ProjectLauncher({ forcedState }) {
|
|
23
29
|
const studio = useStudio();
|
|
@@ -49,7 +55,7 @@ export function ProjectLauncher({ forcedState }) {
|
|
|
49
55
|
? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'
|
|
50
56
|
: 'Open and manage your Elytra projects.' })] }), _jsx(Spacer, {})] }), recents.length > 0 && !forced ? (_jsxs(Stack, { gap: "2", className: "mb-6", children: [_jsx(Text, { size: "xs", tone: "muted", mono: true, className: "uppercase", children: translate('shell.projectList.recent') }), _jsx(Row, { gap: "2", wrap: true, children: recents.map((p) => (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: p.id, _splat: studioPath.index() }, onClick: () => studio.activate(p.id), className: "inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted", children: [_jsx("span", { className: "grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground", children: p.name.charAt(0).toUpperCase() }), p.name] }, p.id))) })] })) : null, forced ? (_jsx(SectionStateView, { state: forced, title: forced === 'empty' ? translate('shell.projectList.empty.title') : undefined })) : visible.length === 0 ? (_jsx(SectionStateView, { state: "empty", title: translate('shell.projectList.empty.title'), description: hasConfig
|
|
51
57
|
? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'
|
|
52
|
-
: 'No active projects.
|
|
58
|
+
: 'No active projects. Projects are code: scaffold a new one with `npm create elytra@latest`, or declare it in elytra.config.ts, then run `elytra init` to provision its backend.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: visible.map((entry) => entry.kind === 'project' ? (_jsx(ProjectCard, { project: entry.project }, entry.project.id)) : (_jsx(UnprovisionedCard, { config: entry.config }, `config:${entry.config.slug}`))) }))] }));
|
|
53
59
|
}
|
|
54
60
|
function ProjectCard({ project }) {
|
|
55
61
|
const studio = useStudio();
|
|
@@ -63,6 +69,6 @@ function ProjectCard({ project }) {
|
|
|
63
69
|
* deterministic state pointing at the CLI provisioning flow — never an error.
|
|
64
70
|
*/
|
|
65
71
|
function UnprovisionedCard({ config }) {
|
|
66
|
-
return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }),
|
|
72
|
+
return (_jsxs(Card, { className: "flex flex-col gap-3 border-dashed p-4", children: [_jsxs(Row, { gap: "2", children: [_jsx("span", { className: "grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground", children: config.name.charAt(0).toUpperCase() }), _jsxs(Stack, { gap: "1", className: "min-w-0", children: [_jsx(Text, { weight: "semibold", className: "truncate", children: config.name }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate", children: config.slug })] }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsxs(Text, { size: "sm", tone: "muted", children: ["Declared in the workspace config, but no backend project exists yet. Point", ' ', _jsx(Code, { tone: "muted", children: "studio/.env" }), " at this project\u2019s Convex deployment, then provision it from that project\u2019s repo:"] }), _jsx(Code, { className: "block w-fit max-w-full truncate text-xs", children: "elytra init" }), _jsxs(Text, { size: "xs", tone: "muted", children: [_jsx(Code, { tone: "muted", children: "elytra doctor" }), " reports what is still missing."] })] }));
|
|
67
73
|
}
|
|
68
74
|
//# sourceMappingURL=project-list.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n NavigateOnce,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useLocation,\n} from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — `elytra.config.ts` + `builder init\n * --project <slug>` — so there is no in-UI create flow. Without a config the\n * adapter-backed list (and the starter-kit gallery) behaves as before.\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n const location = useLocation()\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n // EC-401: a single-project workspace skips the launcher hop — the bare `/`\n // hit forwards straight into the sole project (replace, so Back never loops\n // through the launcher). Explicit \"all projects\" navigations carry the\n // history-state marker and keep the launcher visible, as do forced `?state=`\n // verification states and a lone UNPROVISIONED entry (its setup gate lives\n // here). Activation/recents are recorded by the project screen on arrival.\n const autoForward = launcherAutoForwardTarget({\n entries,\n forcedState: forcedState != null,\n explicitVisit: isExplicitLauncherVisit(location.state),\n })\n if (autoForward) {\n // EC-441: `NavigateOnce`, not `<Navigate>` — on a slow boot the launcher\n // stays mounted while the project splat's modules load, re-rendering on\n // every router-store tick; a per-render re-navigate compounded into the\n // \"Maximum update depth exceeded\" rejection (104 re-fires on one cold load).\n return (\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: autoForward.id, _splat: studioPath.index() }}\n replace\n />\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Create one from the CLI: run `builder init` in your repo to scaffold elytra.config.ts, then `builder init --project <slug>`.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Provision it from your\n repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">\n builder init --project {config.slug}\n </Code>\n </Card>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-list.js","sourceRoot":"","sources":["../../src/components/project-list.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,uBAAuB,EACvB,IAAI,EACJ,YAAY,EACZ,oBAAoB,EACpB,UAAU,EACV,WAAW,GACZ,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAO5E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,WAAW,EAAwB;IACnE,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,kBAAkB,EAAE,CAAA;IAEnD,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,KAAK,QAAQ,CAAA;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAA;IACvB,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC1C,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAE9B,MAAM,MAAM,GAAG,WAAW,IAAI,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA;IAE5E,2EAA2E;IAC3E,4EAA4E;IAC5E,uEAAuE;IACvE,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,WAAW,GAAG,yBAAyB,CAAC;QAC5C,OAAO;QACP,WAAW,EAAE,WAAW,IAAI,IAAI;QAChC,aAAa,EAAE,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC;KACvD,CAAC,CAAA;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,6EAA6E;QAC7E,OAAO,CACL,KAAC,YAAY,IACX,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EACjE,OAAO,SACP,CACH,CAAA;IACH,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,oCAAoC,aACjD,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,MAAM,aAC3B,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,YAAE,SAAS,CAAC,gBAAgB,CAAC,GAAW,EAE/D,KAAC,eAAe,KAAG,EAClB,SAAS,CAAC,CAAC,CAAC,CAEX,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,cAAc,IAAC,SAAS,EAAC,QAAQ,GAAG,OAAE,SAAS,CAAC,MAAM,IACjD,CACT,CAAC,CAAC,CAAC,IAAI,IACJ,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YACf,SAAS;oCACR,CAAC,CAAC,gGAAgG;oCAClG,CAAC,CAAC,uCAAuC,GACtC,IACD,EACR,KAAC,MAAM,KAAG,IACN,EAEL,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAC/B,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,MAAM,aAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,QAAC,SAAS,EAAC,WAAW,YACpD,SAAS,CAAC,0BAA0B,CAAC,GACjC,EACP,KAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,kBACd,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAClB,MAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EACvD,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EACpC,SAAS,EAAC,qJAAqJ,aAE/J,eAAM,SAAS,EAAC,yGAAyG,YACtH,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAC1B,EACN,CAAC,CAAC,IAAI,KATF,CAAC,CAAC,EAAE,CAUJ,CACR,CAAC,GACE,IACA,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,MAAM,CAAC,CAAC,CAAC,CACR,KAAC,gBAAgB,IACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC,CAAC,CAAC,SAAS,GAClF,CACH,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACzB,KAAC,gBAAgB,IACf,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,SAAS,CAAC,+BAA+B,CAAC,EACjD,WAAW,EACT,SAAS;oBACP,CAAC,CAAC,2HAA2H;oBAC7H,CAAC,CAAC,gLAAgL,GAEtL,CACH,CAAC,CAAC,CAAC,CACF,cAAK,SAAS,EAAC,0CAA0C,YACtD,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACrB,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CACzB,KAAC,WAAW,IAAwB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAxC,KAAK,CAAC,OAAO,CAAC,EAAE,CAA4B,CAC/D,CAAC,CAAC,CAAC,CACF,KAAC,iBAAiB,IAAqC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAnD,UAAU,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAA0B,CAChF,CACF,GACG,CACP,IACG,CACP,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,OAAO,EAAwB;IACpD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;IAC1B,+EAA+E;IAC/E,+EAA+E;IAC/E,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAC,mEAAmE,aACjF,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,eAAM,SAAS,EAAC,yGAAyG,YACtH,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAChC,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,SAAS,aAChC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,OAAO,CAAC,IAAI,GACR,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,2BAA2B,YACrD,OAAO,CAAC,IAAI,GACR,IACD,EACR,KAAC,MAAM,KAAG,EACT,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAC,iBAAiB,KAAG,IAClC,EAEN,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,uBAAuB,aAC5C,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,KAAK,IAAC,SAAS,EAAC,QAAQ,GAAG,OAAE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,IACvD,EACR,KAAC,MAAM,KAAG,IACN,EAEN,KAAC,MAAM,IAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAC,IAAI,EAAC,OAAO,QAAC,SAAS,EAAC,MAAM,YACnF,KAAC,IAAI,IACH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,EAC7D,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,YAEzC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAC9C,8BACE,KAAC,KAAK,KAAG,OAAE,SAAS,CAAC,aAAa,CAAC,IAClC,CACJ,GACI,GACA,IACJ,CACR,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,EAAE,MAAM,EAAwC;IACzE,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAC,uCAAuC,aACrD,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,eAAM,SAAS,EAAC,0HAA0H,YACvI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAC/B,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,SAAS,aAChC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,MAAM,CAAC,IAAI,GACP,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,2BAA2B,YACrD,MAAM,CAAC,IAAI,GACP,IACD,EACR,KAAC,MAAM,KAAG,EACV,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,SAAS,CAAC,8BAA8B,CAAC,GAAS,IACxE,EAEN,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,2FACiD,GAAG,EAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,4BAAmB,mGAEhC,EACP,KAAC,IAAI,IAAC,SAAS,EAAC,yCAAyC,4BAAmB,EAC5E,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,aAC1B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,8BAAqB,uCAClC,IACF,CACR,CAAA;AACH,CAAC","sourcesContent":["import {\n isExplicitLauncherVisit,\n Link,\n NavigateOnce,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useLocation,\n} from '@elytracms/studio/router'\nimport { Globe, LogIn, TerminalSquare } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject, SectionState } from '@elytracms/core/studio-core'\nimport { Badge, Button, Card, Code, Heading, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { launcherAutoForwardTarget } from '@elytracms/studio/lib/workspace'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { PlaygroundBadge } from './playground-badge'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\nimport { SectionStateView } from './section-state'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\n\nexport interface ProjectLauncherProps {\n /** Force a deterministic list state for browser verification (?state=…). */\n forcedState?: SectionState | null\n}\n\n/**\n * Studio launcher / project list (brief §4.3, §8.1; reframed by EC-154). The\n * Studio's front door: lists the workspace's projects and routes into a\n * project's shell. With a workspace config the list is config-driven\n * (configured projects ∩ grants, plus explicit \"not provisioned\" states);\n * project *creation* is code-first — declare it in `elytra.config.ts`, then\n * provision the backend with `elytra init` — so there is no in-UI create flow.\n * Without a config the adapter-backed list (and the starter-kit gallery)\n * behaves as before.\n *\n * EC-470: this used to instruct `builder init --project <slug>`. No `builder`\n * bin was ever declared, so that was an unrunnable command shown as product UI.\n * The real CLI is `elytra` (`init` provisions Convex + auth keys; `doctor`\n * reports what is still missing).\n */\nexport function ProjectLauncher({ forcedState }: ProjectLauncherProps) {\n const studio = useStudio()\n const { entries, workspace } = useSwitcherEntries()\n\n const hasConfig = workspace.kind === 'config'\n const visible = entries\n const recents = useRecentProjects(entries)\n const location = useLocation()\n\n const forced = forcedState && forcedState !== 'working' ? forcedState : null\n\n // EC-401: a single-project workspace skips the launcher hop — the bare `/`\n // hit forwards straight into the sole project (replace, so Back never loops\n // through the launcher). Explicit \"all projects\" navigations carry the\n // history-state marker and keep the launcher visible, as do forced `?state=`\n // verification states and a lone UNPROVISIONED entry (its setup gate lives\n // here). Activation/recents are recorded by the project screen on arrival.\n const autoForward = launcherAutoForwardTarget({\n entries,\n forcedState: forcedState != null,\n explicitVisit: isExplicitLauncherVisit(location.state),\n })\n if (autoForward) {\n // EC-441: `NavigateOnce`, not `<Navigate>` — on a slow boot the launcher\n // stays mounted while the project splat's modules load, re-rendering on\n // every router-store tick; a per-render re-navigate compounded into the\n // \"Maximum update depth exceeded\" rejection (104 re-fires on one cold load).\n return (\n <NavigateOnce\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: autoForward.id, _splat: studioPath.index() }}\n replace\n />\n )\n }\n\n return (\n <div className=\"mx-auto w-full max-w-6xl px-6 py-8\">\n <Row gap=\"3\" className=\"mb-6\">\n <Stack gap=\"1\">\n <Row gap=\"2\">\n <Heading size=\"display\">{translate('shell.projects')}</Heading>\n {/* EC-153: explicit playground-mode marker (renders nothing on Convex). */}\n <PlaygroundBadge />\n {hasConfig ? (\n // EC-154: this workspace is defined in code.\n <Badge variant=\"outline\">\n <TerminalSquare className=\"size-3\" /> {workspace.source}\n </Badge>\n ) : null}\n </Row>\n <Text tone=\"muted\">\n {hasConfig\n ? 'Projects are defined in code. Add entries to elytra.config.ts and provision them with the CLI.'\n : 'Open and manage your Elytra projects.'}\n </Text>\n </Stack>\n <Spacer />\n </Row>\n\n {recents.length > 0 && !forced ? (\n <Stack gap=\"2\" className=\"mb-6\">\n <Text size=\"xs\" tone=\"muted\" mono className=\"uppercase\">\n {translate('shell.projectList.recent')}\n </Text>\n <Row gap=\"2\" wrap>\n {recents.map((p) => (\n <Link\n key={p.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: p.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(p.id)}\n className=\"inline-flex items-center gap-2 rounded-md border border-border bg-card px-3 py-1.5 text-sm transition-colors hover:border-primary/40 hover:bg-muted\"\n >\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {p.name.charAt(0).toUpperCase()}\n </span>\n {p.name}\n </Link>\n ))}\n </Row>\n </Stack>\n ) : null}\n\n {forced ? (\n <SectionStateView\n state={forced}\n title={forced === 'empty' ? translate('shell.projectList.empty.title') : undefined}\n />\n ) : visible.length === 0 ? (\n <SectionStateView\n state=\"empty\"\n title={translate('shell.projectList.empty.title')}\n description={\n hasConfig\n ? 'The workspace config declares no visible projects. Add an entry to elytra.config.ts, or ask an admin to grant you access.'\n : 'No active projects. Projects are code: scaffold a new one with `npm create elytra@latest`, or declare it in elytra.config.ts, then run `elytra init` to provision its backend.'\n }\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {visible.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectCard key={entry.project.id} project={entry.project} />\n ) : (\n <UnprovisionedCard key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction ProjectCard({ project }: { project: Project }) {\n const studio = useStudio()\n // Per-backend sign-in state (EC-179): one project per backend, so a project on\n // a deployment the user hasn't authenticated into shows \"Sign in\", not \"Open\".\n const authed = hasProjectSession(project.slug)\n return (\n <Card className=\"flex flex-col gap-3 p-4 transition-colors hover:border-primary/30\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-sm font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {project.slug}\n </Code>\n </Stack>\n <Spacer />\n {authed ? null : <SignInNeededBadge />}\n </Row>\n\n <Row gap=\"2\" className=\"text-muted-foreground\">\n <Badge variant=\"outline\">\n <Globe className=\"size-3\" /> {project.settings.locales.length}\n </Badge>\n <Spacer />\n </Row>\n\n <Button variant={authed ? 'secondary' : 'default'} size=\"sm\" asChild className=\"mt-1\">\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: project.id, _splat: studioPath.index() }}\n onClick={() => studio.activate(project.id)}\n >\n {authed ? translate('shell.projectCard.open') : (\n <>\n <LogIn /> {translate('auth.signIn')}\n </>\n )}\n </Link>\n </Button>\n </Card>\n )\n}\n\n/**\n * A configured project that has no backend project yet (EC-154): an explicit,\n * deterministic state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedCard({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <Card className=\"flex flex-col gap-3 border-dashed p-4\">\n <Row gap=\"2\">\n <span className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-border bg-muted text-sm font-bold text-muted-foreground\">\n {config.name.charAt(0).toUpperCase()}\n </span>\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {config.name}\n </Text>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate\">\n {config.slug}\n </Code>\n </Stack>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n\n <Text size=\"sm\" tone=\"muted\">\n Declared in the workspace config, but no backend project exists yet. Point{' '}\n <Code tone=\"muted\">studio/.env</Code> at this project’s Convex deployment, then provision it\n from that project’s repo:\n </Text>\n <Code className=\"block w-fit max-w-full truncate text-xs\">elytra init</Code>\n <Text size=\"xs\" tone=\"muted\">\n <Code tone=\"muted\">elytra doctor</Code> reports what is still missing.\n </Text>\n </Card>\n )\n}\n"]}
|
|
@@ -4,7 +4,9 @@ import type { Project } from '@elytracms/core/studio-core';
|
|
|
4
4
|
* the list is config-driven — configured projects ∩ the user's grants, plus
|
|
5
5
|
* explicit "not provisioned" states for config entries that have no backend
|
|
6
6
|
* project yet. Without a config it lists the adapter-backed projects exactly
|
|
7
|
-
* as before. Project *creation* lives in code + CLI (`
|
|
7
|
+
* as before. Project *creation* lives in code + CLI (`elytra.config.ts` +
|
|
8
|
+
* `elytra init`), not here — EC-470 retired the `builder init` this named,
|
|
9
|
+
* which was never a declared bin.
|
|
8
10
|
*/
|
|
9
11
|
export declare function ProjectSwitcher({ activeProject }: {
|
|
10
12
|
activeProject: Project;
|
|
@@ -12,7 +12,9 @@ import { SignInNeededBadge } from './sign-in-needed-badge.js';
|
|
|
12
12
|
* the list is config-driven — configured projects ∩ the user's grants, plus
|
|
13
13
|
* explicit "not provisioned" states for config entries that have no backend
|
|
14
14
|
* project yet. Without a config it lists the adapter-backed projects exactly
|
|
15
|
-
* as before. Project *creation* lives in code + CLI (`
|
|
15
|
+
* as before. Project *creation* lives in code + CLI (`elytra.config.ts` +
|
|
16
|
+
* `elytra init`), not here — EC-470 retired the `builder init` this named,
|
|
17
|
+
* which was never a declared bin.
|
|
16
18
|
*/
|
|
17
19
|
export function ProjectSwitcher({ activeProject }) {
|
|
18
20
|
const studio = useStudio();
|
|
@@ -47,6 +49,6 @@ function ProjectMenuItem({ project, active, onSelect, }) {
|
|
|
47
49
|
* non-navigating state pointing at the CLI provisioning flow — never an error.
|
|
48
50
|
*/
|
|
49
51
|
function UnprovisionedMenuItem({ config }) {
|
|
50
|
-
return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }),
|
|
52
|
+
return (_jsx(DropdownMenuItem, { disabled: true, className: "gap-2 opacity-100", children: _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1 py-0.5", children: [_jsxs(Row, { gap: "2", className: "min-w-0", children: [_jsx(Text, { size: "sm", tone: "muted", className: "truncate", children: config.name }), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: translate('shell.project.notProvisioned') })] }), _jsx(Code, { tone: "muted", className: "w-fit max-w-full truncate text-[0.7rem]", children: "elytra init" })] }) }));
|
|
51
53
|
}
|
|
52
54
|
//# sourceMappingURL=project-switcher.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`builder init`), not here.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n builder init --project {config.slug}\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
|
|
1
|
+
{"version":3,"file":"project-switcher.js","sourceRoot":"","sources":["../../src/components/project-switcher.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n launcherVisitState,\n STUDIO_PROJECT_ROUTE,\n studioPath,\n useNavigate,\n} from '@elytracms/studio/router'\nimport { Check, ChevronsUpDown, FolderOpen } from 'lucide-react'\nimport type { Project, ResolvedWorkspaceProject } from '@elytracms/core/studio-core'\nimport { hasProjectSession } from '@elytracms/studio/lib/auth'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Heading,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useStudio } from './studio-context'\nimport { useRecentProjects, useSwitcherEntries } from './workspace-projects'\nimport { SignInNeededBadge } from './sign-in-needed-badge'\n\n/**\n * The project switcher (EC-033, reframed by EC-154): with a workspace config\n * the list is config-driven — configured projects ∩ the user's grants, plus\n * explicit \"not provisioned\" states for config entries that have no backend\n * project yet. Without a config it lists the adapter-backed projects exactly\n * as before. Project *creation* lives in code + CLI (`elytra.config.ts` +\n * `elytra init`), not here — EC-470 retired the `builder init` this named,\n * which was never a declared bin.\n */\nexport function ProjectSwitcher({ activeProject }: { activeProject: Project }) {\n const studio = useStudio()\n const navigate = useNavigate()\n const { entries } = useSwitcherEntries()\n\n const activeEntries = entries\n const recents = useRecentProjects(activeEntries, { exclude: activeProject.id })\n\n function open(project: Project) {\n studio.activate(project.id)\n navigate({ to: STUDIO_PROJECT_ROUTE, params: { projectId: project.id, _splat: studioPath.index() } })\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-2 px-2.5\">\n <span className=\"grid size-4 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {activeProject.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate font-medium\">{activeProject.name}</span>\n <ChevronsUpDown className=\"size-3.5 text-muted-foreground\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"w-80\" align=\"start\">\n {recents.length > 0 ? (\n <>\n <DropdownMenuLabel>{translate('shell.projectSwitcher.recent')}</DropdownMenuLabel>\n {recents.map((p) => (\n <ProjectMenuItem key={p.id} project={p} active={false} onSelect={() => open(p)} />\n ))}\n <DropdownMenuSeparator />\n </>\n ) : null}\n <DropdownMenuLabel>{translate('shell.projects')}</DropdownMenuLabel>\n {activeEntries.map((entry) =>\n entry.kind === 'project' ? (\n <ProjectMenuItem\n key={entry.project.id}\n project={entry.project}\n active={entry.project.id === activeProject.id}\n onSelect={() => open(entry.project)}\n />\n ) : (\n <UnprovisionedMenuItem key={`config:${entry.config.slug}`} config={entry.config} />\n ),\n )}\n <DropdownMenuSeparator />\n {/* EC-401: an explicit \"all projects\" navigation — marked so the\n launcher's single-project auto-forward never bounces it back. */}\n <DropdownMenuItem onSelect={() => navigate({ to: '/', state: launcherVisitState })}>\n <FolderOpen /> {translate('shell.projectSwitcher.browseAll')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/**\n * The switcher slot's single-project face (EC-401): the active project's name\n * as a plain heading — same identity mark as the switcher trigger, but no\n * button, no chevron, no dropdown. `StudioShell` renders this instead of\n * {@link ProjectSwitcher} while `isSingleProjectWorkspace(entries)` holds; the\n * switcher returns the moment a second workspace entry exists.\n */\nexport function ActiveProjectName({ project }: { project: Project }) {\n return (\n <Heading level={1} size=\"h4\" className=\"flex h-8 min-w-0 items-center gap-2 px-2.5\">\n <span className=\"grid size-4 shrink-0 place-items-center rounded-[4px] bg-primary text-[0.6rem] font-bold text-primary-foreground\">\n {project.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"max-w-[12rem] truncate\">{project.name}</span>\n </Heading>\n )\n}\n\nfunction ProjectMenuItem({\n project,\n active,\n onSelect,\n}: {\n project: Project\n active: boolean\n onSelect: () => void\n}) {\n // Per-backend sign-in state (EC-179): the active project is signed-in by\n // definition; others wear a \"Sign in\" badge when their backend has no session.\n const needsSignIn = !active && !hasProjectSession(project.slug)\n return (\n <DropdownMenuItem onSelect={onSelect} className=\"gap-2\">\n <Row gap=\"2\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" className=\"truncate\">\n {project.name}\n </Text>\n <Code tone=\"muted\" className=\"shrink-0\">\n {project.slug}\n </Code>\n <Spacer />\n {needsSignIn ? <SignInNeededBadge /> : null}\n {active ? <Check className=\"size-4 text-primary\" /> : null}\n </Row>\n </DropdownMenuItem>\n )\n}\n\n/**\n * A configured project with no backend project yet (EC-154): a clear,\n * non-navigating state pointing at the CLI provisioning flow — never an error.\n */\nfunction UnprovisionedMenuItem({ config }: { config: ResolvedWorkspaceProject }) {\n return (\n <DropdownMenuItem disabled className=\"gap-2 opacity-100\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1 py-0.5\">\n <Row gap=\"2\" className=\"min-w-0\">\n <Text size=\"sm\" tone=\"muted\" className=\"truncate\">\n {config.name}\n </Text>\n <Spacer />\n <Badge variant=\"outline\">{translate('shell.project.notProvisioned')}</Badge>\n </Row>\n <Code tone=\"muted\" className=\"w-fit max-w-full truncate text-[0.7rem]\">\n elytra init\n </Code>\n </Stack>\n </DropdownMenuItem>\n )\n}\n\n"]}
|
|
@@ -40,6 +40,27 @@ export interface RedirectsStore {
|
|
|
40
40
|
refresh(): Promise<void>;
|
|
41
41
|
}
|
|
42
42
|
declare const useRedirects: () => RedirectsStore;
|
|
43
|
+
/**
|
|
44
|
+
* The project's redirects, read through the shared operations cache — the ONE query
|
|
45
|
+
* this rail uses, without needing {@link RedirectsProvider}.
|
|
46
|
+
*
|
|
47
|
+
* Extracted (EC-469) so the section rail's URL-map validation can read redirects from
|
|
48
|
+
* outside the redirects module while sharing this exact query key. One key, one cache
|
|
49
|
+
* entry (AD-17): the rail's foot and the rail itself are the same query, so opening
|
|
50
|
+
* Redirects hits a warm cache, and `save`/`remove`'s `cache.patch` below updates both
|
|
51
|
+
* at once — a fixed loop stops being reported the moment it is fixed.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useRedirectList(projectId: string): import("@tanstack/react-query").UseQueryResult<{
|
|
54
|
+
createdAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
id: string;
|
|
57
|
+
projectId: string;
|
|
58
|
+
from: string;
|
|
59
|
+
to: string;
|
|
60
|
+
permanent: boolean;
|
|
61
|
+
enabled: boolean;
|
|
62
|
+
meta?: Record<string, unknown> | undefined;
|
|
63
|
+
}[]>;
|
|
43
64
|
export declare function RedirectsProvider({ projectId, children, }: {
|
|
44
65
|
projectId: string;
|
|
45
66
|
children: ReactNode;
|
|
@@ -20,14 +20,27 @@ async function loadRedirects(operations, projectId) {
|
|
|
20
20
|
throw new Error(result.error.message);
|
|
21
21
|
return [...result.value];
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The project's redirects, read through the shared operations cache — the ONE query
|
|
25
|
+
* this rail uses, without needing {@link RedirectsProvider}.
|
|
26
|
+
*
|
|
27
|
+
* Extracted (EC-469) so the section rail's URL-map validation can read redirects from
|
|
28
|
+
* outside the redirects module while sharing this exact query key. One key, one cache
|
|
29
|
+
* entry (AD-17): the rail's foot and the rail itself are the same query, so opening
|
|
30
|
+
* Redirects hits a warm cache, and `save`/`remove`'s `cache.patch` below updates both
|
|
31
|
+
* at once — a fixed loop stops being reported the moment it is fixed.
|
|
32
|
+
*/
|
|
33
|
+
export function useRedirectList(projectId) {
|
|
34
|
+
return useOperationQuery({
|
|
35
|
+
queryKey: redirectsQueryKey(projectId),
|
|
36
|
+
loader: (operations) => loadRedirects(operations, projectId),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
23
39
|
function byFrom(a, b) {
|
|
24
40
|
return a.from < b.from ? -1 : a.from > b.from ? 1 : 0;
|
|
25
41
|
}
|
|
26
42
|
export function RedirectsProvider({ projectId, children, }) {
|
|
27
|
-
const query =
|
|
28
|
-
queryKey: redirectsQueryKey(projectId),
|
|
29
|
-
loader: (operations) => loadRedirects(operations, projectId),
|
|
30
|
-
});
|
|
43
|
+
const query = useRedirectList(projectId);
|
|
31
44
|
const cache = useOperationCache();
|
|
32
45
|
const refetch = query.refetch;
|
|
33
46
|
const persisted = query.data ?? EMPTY_REDIRECTS;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RedirectsContext, useRequired: useRedirects } =\n createRequiredContext<RedirectsStore>('Redirects')\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const { status, errorMessage } = listStoreState(query)\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n // New redirects default to enabled — the reason you create one is for it to\n // work; `enabled` is really the switch for *pausing* an existing redirect. A\n // half-filled draft can't go live anyway (save requires from/to).\n enabled: true,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n ...(input.meta !== undefined ? { meta: input.meta } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport { useRedirects }\n"]}
|
|
1
|
+
{"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { listStoreState, useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst { Context: RedirectsContext, useRequired: useRedirects } =\n createRequiredContext<RedirectsStore>('Redirects')\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\n/**\n * The project's redirects, read through the shared operations cache — the ONE query\n * this rail uses, without needing {@link RedirectsProvider}.\n *\n * Extracted (EC-469) so the section rail's URL-map validation can read redirects from\n * outside the redirects module while sharing this exact query key. One key, one cache\n * entry (AD-17): the rail's foot and the rail itself are the same query, so opening\n * Redirects hits a warm cache, and `save`/`remove`'s `cache.patch` below updates both\n * at once — a fixed loop stops being reported the moment it is fixed.\n */\nexport function useRedirectList(projectId: string) {\n return useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useRedirectList(projectId)\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const { status, errorMessage } = listStoreState(query)\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n // New redirects default to enabled — the reason you create one is for it to\n // work; `enabled` is really the switch for *pausing* an existing redirect. A\n // half-filled draft can't go live anyway (save requires from/to).\n enabled: true,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n ...(input.meta !== undefined ? { meta: input.meta } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport { useRedirects }\n"]}
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
|
|
4
4
|
import { AlertTriangle, ChevronDown, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
|
5
5
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
6
|
import { readCollapsedViews, storeCollapsedViews } from '@elytracms/studio/lib/rail';
|
|
7
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
8
|
+
import { projectConfigIssues, resolveDeploymentCollections, resolveDeploymentReuse, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
|
|
9
|
+
import { routeValidationIssues } from '@elytracms/studio/lib/routes/route-issues';
|
|
7
10
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
11
|
+
import { cmsIssueLabel, cmsValidationIssueMessage, customizationIssueMessage, } from '@elytracms/studio/lib/persona/labels';
|
|
8
12
|
import { Text } from '@elytracms/studio/ui';
|
|
9
13
|
import { activeViewIdFromPath, isNavViewVisible, useStudioNav } from './app-views/sidebar.js';
|
|
14
|
+
import { resolveReuse } from './builder/reuse.js';
|
|
15
|
+
import { useRedirectList } from './redirects/redirects-store.js';
|
|
10
16
|
import { useFieldRenderers } from './cms/field-renderer.js';
|
|
11
17
|
import { useProjectRole } from './auth-context.js';
|
|
18
|
+
/**
|
|
19
|
+
* Customization issues, as rail-foot items — localized (EC-478). The producers
|
|
20
|
+
* (`studio-core/customization.ts`, `default-modules.tsx`, `builder/reuse.ts`) emit a
|
|
21
|
+
* catalog key + identifier values rather than a sentence, because the EC-236 seam
|
|
22
|
+
* lives in `packages/core` and may not import the studio catalog;
|
|
23
|
+
* {@link customizationIssueMessage} resolves them here, so this list renders wholly in
|
|
24
|
+
* the active locale instead of half of it.
|
|
25
|
+
*/
|
|
26
|
+
function customizationItems(issues) {
|
|
27
|
+
return issues.map((issue, index) => ({
|
|
28
|
+
key: `${issue.point}:${issue.id ?? index}`,
|
|
29
|
+
message: customizationIssueMessage(issue),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
12
32
|
/**
|
|
13
33
|
* Vertical section rail (brief §8.1), driven by the configurable sidebar (EC-237):
|
|
14
34
|
* a consumer-extendable tree of views / groups / separators resolved over the
|
|
@@ -24,8 +44,16 @@ export function SectionNav({ project, collapsed, onToggleCollapsed }) {
|
|
|
24
44
|
const activeViewId = activeViewIdFromPath(pathname);
|
|
25
45
|
const { nodes, issues: navIssues } = useStudioNav(project);
|
|
26
46
|
const { issues: rendererIssues } = useFieldRenderers();
|
|
27
|
-
const
|
|
47
|
+
const configIssues = useProjectConfigIssues();
|
|
48
|
+
const reuseIssues = useReuseIssues();
|
|
28
49
|
const projectId = project.id;
|
|
50
|
+
const routeIssues = useRouteIssues(projectId);
|
|
51
|
+
// Config issues lead: "this project declares no collections" explains every
|
|
52
|
+
// other emptiness on the screen, so it is the first thing to read.
|
|
53
|
+
const issues = [
|
|
54
|
+
...customizationItems([...configIssues, ...navIssues, ...rendererIssues, ...reuseIssues]),
|
|
55
|
+
...routeIssues,
|
|
56
|
+
];
|
|
29
57
|
// EC-240 (AD-18): per-nav-item, default-open permission gating. The signed-in
|
|
30
58
|
// user's role drives an OPTIONAL `visible` predicate on any view; absent →
|
|
31
59
|
// visible. UI reflection only — the server chokepoint stays the real boundary,
|
|
@@ -78,6 +106,73 @@ export function SectionNav({ project, collapsed, onToggleCollapsed }) {
|
|
|
78
106
|
: node.children?.filter(isVisible).map((child) => (_jsx(NavViewLink, { projectId: projectId, view: child, activeViewId: activeViewId, collapsed: collapsed, nested: true }, child.id)))] }, node.id));
|
|
79
107
|
}), _jsxs("div", { className: "mt-auto flex flex-col gap-0.5", children: [issues.length > 0 ? _jsx(NavIssues, { issues: issues, collapsed: collapsed }) : null, _jsx("button", { type: "button", onClick: onToggleCollapsed, "aria-label": collapsed ? translate('shell.sidebar.expand') : translate('shell.sidebar.collapse'), "aria-pressed": collapsed, title: collapsed ? translate('shell.sidebar.expand') : translate('shell.sidebar.collapse'), className: cn('flex h-9 items-center gap-2.5 rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground', collapsed ? 'justify-center px-0' : 'px-3'), children: collapsed ? (_jsx(PanelLeftOpen, { className: "size-4 shrink-0" })) : (_jsxs(_Fragment, { children: [_jsx(PanelLeftClose, { className: "size-4 shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: translate('shell.sidebar.collapseLabel') })] })) })] })] }));
|
|
80
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* EC-479 — what the project has not declared but the studio cannot work without.
|
|
111
|
+
*
|
|
112
|
+
* Joins the rail foot for the same reason EC-467's and EC-469's findings do: it is a
|
|
113
|
+
* project-CONFIGURATION problem, not a content problem in one document, and the rail
|
|
114
|
+
* is on every studio surface. It exists at all because the engine used to hide this
|
|
115
|
+
* state behind its own `cms-core` fixtures — an unconfigured studio showed sample
|
|
116
|
+
* collections and sample documents as if they were the project's. Removing that
|
|
117
|
+
* fallback without reporting the absence would just trade a wrong screen for a blank
|
|
118
|
+
* one, which the house rule bans equally.
|
|
119
|
+
*
|
|
120
|
+
* Read from the config holder alone (static per studio boot, like `useReuseIssues`),
|
|
121
|
+
* so it needs no CMS provider and is memoised once. The pure decision core is
|
|
122
|
+
* `projectConfigIssues`.
|
|
123
|
+
*/
|
|
124
|
+
function useProjectConfigIssues() {
|
|
125
|
+
return useMemo(() => projectConfigIssues(resolveDeploymentCollections()), []);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* EC-467 — the project's reusable-elements config issues: a declaration that
|
|
129
|
+
* cannot resolve, or an ambiguous inference the studio refuses to guess through.
|
|
130
|
+
* They join the rail foot because they are project-CONFIGURATION problems (like the
|
|
131
|
+
* rail's own and the field renderers'), not content problems in one document — and
|
|
132
|
+
* the rail is on every studio surface, so the developer cannot miss them. A project
|
|
133
|
+
* with an unambiguous embed, or with none at all, contributes nothing here.
|
|
134
|
+
*
|
|
135
|
+
* Resolved from the config holders alone (registry + config collections + studio
|
|
136
|
+
* config), all static per studio boot, so this needs no CMS provider and is
|
|
137
|
+
* memoised once. The pure decision core is `resolveReuse`.
|
|
138
|
+
*/
|
|
139
|
+
function useReuseIssues() {
|
|
140
|
+
return useMemo(() => resolveReuse(getStudioRegistry().registry.list(), resolveDeploymentCollections(), resolveDeploymentReuse()).issues, []);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* EC-469 — the project's URL-map issues: two routes that match the same URLs, or a
|
|
144
|
+
* redirect chain that loops.
|
|
145
|
+
*
|
|
146
|
+
* They join the rail foot for the same reason EC-467's reusable-elements issues do:
|
|
147
|
+
* they are project-level problems, not content problems in one document, so the
|
|
148
|
+
* document-scoped issues pane has no place for them — and the rail is on every studio
|
|
149
|
+
* surface, so the developer cannot miss them. This is emphatically not a revived
|
|
150
|
+
* Routes view (EC-303 retired that, correctly): nothing here is editable, it is
|
|
151
|
+
* validation of the code + data the studio already reads.
|
|
152
|
+
*
|
|
153
|
+
* The two halves come from different planes, which is why only the studio can show
|
|
154
|
+
* both: routes are CODE (config, static per boot) while redirects are editorial DATA
|
|
155
|
+
* (the rail, live). The redirects read shares the rail's own query key, so a loop
|
|
156
|
+
* disappears from the foot the moment it is fixed — and while that query is loading or
|
|
157
|
+
* failed, `data` is absent and only the code-side conflicts are reported. That is
|
|
158
|
+
* honest: the rail itself surfaces its own load error.
|
|
159
|
+
*
|
|
160
|
+
* Copy follows the issues-pane convention (EC-192): the localized plain-language
|
|
161
|
+
* label, then the detail — both localized since EC-487, which gave `CmsValidationIssue`
|
|
162
|
+
* the same catalog-reference carriage EC-478 gave `CustomizationIssue`. Before that the
|
|
163
|
+
* label was German beside an English sentence, in the same list as the (already
|
|
164
|
+
* localized) customization issues.
|
|
165
|
+
*/
|
|
166
|
+
function useRouteIssues(projectId) {
|
|
167
|
+
const redirects = useRedirectList(projectId).data;
|
|
168
|
+
return useMemo(() => {
|
|
169
|
+
const issues = routeValidationIssues(resolveDeploymentRoutes(), redirects ?? []);
|
|
170
|
+
return issues.map((issue, index) => ({
|
|
171
|
+
key: `route-issue:${issue.code}:${issue.path.join('/') || index}`,
|
|
172
|
+
message: `${cmsIssueLabel(issue.code)} — ${cmsValidationIssueMessage(issue)}`,
|
|
173
|
+
}));
|
|
174
|
+
}, [redirects]);
|
|
175
|
+
}
|
|
81
176
|
/** One rail view (top-level or nested) — a Link to its home route, active by view id. */
|
|
82
177
|
function NavViewLink({ projectId, view, activeViewId, collapsed, nested = false, hasActiveChild = false, collapsible = false, expanded = true, onToggleExpanded, }) {
|
|
83
178
|
const isActive = view.id === activeViewId;
|
|
@@ -98,11 +193,14 @@ function NavViewLink({ projectId, view, activeViewId, collapsed, nested = false,
|
|
|
98
193
|
name: view.label,
|
|
99
194
|
}), "aria-expanded": expanded, className: "absolute right-1 top-1/2 grid size-6 -translate-y-1/2 place-items-center rounded text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground", children: _jsx(ChevronDown, { className: cn('size-3.5 transition-transform', expanded ? '' : '-rotate-90'), "aria-hidden": true }) })] }));
|
|
100
195
|
}
|
|
101
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Visible project-level issues at the rail foot — the seam's "never silent" (EC-236,
|
|
198
|
+
* extended by EC-467's reuse issues and EC-469's URL-map issues).
|
|
199
|
+
*/
|
|
102
200
|
function NavIssues({ issues, collapsed }) {
|
|
103
201
|
if (collapsed) {
|
|
104
202
|
return (_jsx("div", { className: "grid h-9 place-items-center text-amber-600 dark:text-amber-400", title: issues.map((issue) => issue.message).join('\n'), children: _jsx(AlertTriangle, { className: "size-4", "aria-label": translate('shell.nav.issuesCount', { count: issues.length }) }) }));
|
|
105
203
|
}
|
|
106
|
-
return (_jsx("div", { className: "mb-1 flex flex-col gap-1 rounded-md border border-amber-500/30 bg-amber-500/5 p-2", children: issues.map((issue
|
|
204
|
+
return (_jsx("div", { className: "mb-1 flex flex-col gap-1 rounded-md border border-amber-500/30 bg-amber-500/5 p-2", children: issues.map((issue) => (_jsxs("div", { className: "flex items-start gap-1.5 text-amber-600 dark:text-amber-400", title: issue.message, children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": true }), _jsx(Text, { size: "xs", className: "min-w-0 leading-snug", children: issue.message })] }, issue.key))) }));
|
|
107
205
|
}
|
|
108
206
|
//# sourceMappingURL=section-nav.js.map
|