mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433) When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces returns 403 for every caller and the UI hides every "Create workspace" affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This closes the gap where ALLOW_SIGNUP=false still let any signed-in user open an isolated workspace the platform admin couldn't see. - server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace, workspace_creation_disabled in /api/config, Go tests. - frontend: new workspaceCreationDisabled in configStore, hide sidebar entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding StepWorkspace to a "creation disabled, ask for invite" state when the flag is on, EN + zh-Hans locale strings. - ops: .env.example, docker-compose.selfhost, helm values + configmap, SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs (EN + zh). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): drive create path off workspaceCreationAllowed (#3433) PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already has a workspace, StepWorkspace still walked the resume copy (`headline_resume` / `lede_resume` mentioning "or start another") and `creatingActive` ignored the flag, leaving a stale clickable create CTA possible if /api/config arrived late. Refactor StepWorkspace to derive a single `workspaceCreationAllowed` boolean from the config store. It now drives: - Initial `mode` state (defaults to "existing" when disabled + reusing so the CTA is pre-armed for the only valid action). - `creatingActive` so the footer CTA cannot fall back into the create branch even mid-render. - Eyebrow / headline / lede strings — adds `creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for the disabled + reusing variant. Tests: cover the three reachable shapes — flag off + no existing, flag on + no existing, flag on + existing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useNavigation } from "../navigation";
|
|
import { DragStrip } from "../platform";
|
|
import { ArrowLeft } from "lucide-react";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@multica/ui/components/ui/dialog";
|
|
import { paths } from "@multica/core/paths";
|
|
import { useConfigStore } from "@multica/core/config";
|
|
import { CreateWorkspaceForm } from "../workspace/create-workspace-form";
|
|
import { useT } from "../i18n";
|
|
|
|
export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) {
|
|
const { t } = useT("modals");
|
|
const tWorkspace = useT("workspace").t;
|
|
const router = useNavigation();
|
|
const workspaceCreationDisabled = useConfigStore((s) => s.workspaceCreationDisabled);
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
onOpenChange={(v) => {
|
|
if (!v) onClose();
|
|
}}
|
|
>
|
|
<DialogContent
|
|
finalFocus={false}
|
|
showCloseButton={false}
|
|
className="inset-0 flex h-full w-full max-w-none sm:max-w-none translate-0 flex-col rounded-none bg-background ring-0 shadow-none"
|
|
>
|
|
{/* DragStrip as flex child — macOS traffic lights stay visible and
|
|
the top 48px is draggable. Back button sits just below the strip
|
|
(top-16 = 64px), clear of both traffic lights (y<=27) and the
|
|
strip (y<=48). `no-drag` is a belt-and-braces guard in case the
|
|
button's layout ever creeps up into the strip zone. */}
|
|
<DragStrip />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="absolute top-16 left-12 text-muted-foreground"
|
|
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
|
onClick={onClose}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
{t(($) => $.common.back)}
|
|
</Button>
|
|
|
|
<div className="flex flex-1 flex-col items-center justify-center px-6 pb-12">
|
|
<div className="flex w-full max-w-md flex-col items-center gap-6">
|
|
{workspaceCreationDisabled ? (
|
|
<div className="text-center">
|
|
<DialogTitle className="text-2xl font-semibold">
|
|
{tWorkspace(($) => $.creation_disabled.title)}
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-2">
|
|
{tWorkspace(($) => $.creation_disabled.description)}
|
|
</DialogDescription>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="text-center">
|
|
<DialogTitle className="text-2xl font-semibold">
|
|
{t(($) => $.create_workspace.title)}
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-2">
|
|
{t(($) => $.create_workspace.description)}
|
|
</DialogDescription>
|
|
</div>
|
|
<CreateWorkspaceForm
|
|
onSuccess={(newWs) => {
|
|
onClose();
|
|
// Navigate INTO the new workspace. The mutation's own onSuccess
|
|
// (in core/workspace/mutations.ts) runs before this callback and
|
|
// has already seeded the workspace list cache, so the destination
|
|
// [workspaceSlug]/layout will resolve newWs.slug → workspace
|
|
// synchronously without a loading flash.
|
|
router.push(paths.workspace(newWs.slug).issues());
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|