feat(org): add create organization dialog and fix admin role check

- Add CreateOrgDialog component with name/slug form and auto-slug generation
- Add "New Organisation" button in TopBar org dropdown (limited to 10 orgs)
- Fix admin check in AppSidebar to use currently selected org role
  instead of global isOrgAdmin flag for proper org-scoped permissions
This commit is contained in:
2026-04-20 15:04:43 +09:30
parent e5fbbf521d
commit d927c17c60
3 changed files with 211 additions and 5 deletions
+169
View File
@@ -0,0 +1,169 @@
import { useState, useEffect } from "react";
import { Loader2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { api, ApiError, Organization } from "@/lib/api";
interface CreateOrgDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: (org: Organization) => void;
}
function toSlug(name: string): string {
const slug = name
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
return slug || "untitled-org";
}
export function CreateOrgDialog({
open,
onOpenChange,
onSuccess,
}: CreateOrgDialogProps) {
const [orgName, setOrgName] = useState("");
const [orgSlug, setOrgSlug] = useState("");
const [slugTouched, setSlugTouched] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);
const { toast } = useToast();
useEffect(() => {
if (open) {
setOrgName("");
setOrgSlug("");
setSlugTouched(false);
setCreateError(null);
setIsCreating(false);
}
}, [open]);
useEffect(() => {
if (!slugTouched && orgName) {
setOrgSlug(toSlug(orgName));
}
}, [orgName, slugTouched]);
const handleClose = (isOpen: boolean) => {
if (!isOpen) {
setOrgName("");
setOrgSlug("");
setSlugTouched(false);
setCreateError(null);
setIsCreating(false);
}
onOpenChange(isOpen);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const trimmedName = orgName.trim();
const trimmedSlug = orgSlug.trim();
if (!trimmedName || !trimmedSlug) {
return;
}
setIsCreating(true);
setCreateError(null);
try {
const result = await api.organizations.create(trimmedName, trimmedSlug);
toast({
title: "Organisation created",
description: `${result.organization.name} has been created successfully.`,
});
onSuccess?.(result.organization);
handleClose(false);
} catch (err) {
console.error("Failed to create organisation:", err);
if (err instanceof ApiError) {
setCreateError(err.message);
} else {
setCreateError("An error occurred. Please try again.");
}
} finally {
setIsCreating(false);
}
};
const isValid = orgName.trim() && orgSlug.trim();
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Organisation</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="org-name">Organisation name</Label>
<Input
id="org-name"
placeholder="My Organisation"
value={orgName}
onChange={(e) => {
setOrgName(e.target.value);
setCreateError(null);
}}
disabled={isCreating}
autoFocus
/>
</div>
<div className="space-y-2">
<Label htmlFor="org-slug">Slug</Label>
<Input
id="org-slug"
placeholder="my-organisation"
value={orgSlug}
onChange={(e) => {
setOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""));
setSlugTouched(true);
setCreateError(null);
}}
disabled={isCreating}
/>
<p className="text-sm text-muted-foreground">
Used in URLs. Lowercase letters, numbers, and hyphens only.
</p>
</div>
{createError && (
<p className="text-sm text-destructive">{createError}</p>
)}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => handleClose(false)}
disabled={isCreating}
>
Cancel
</Button>
<Button type="submit" disabled={isCreating || !isValid}>
{isCreating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Create
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}