mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
edit space names
This commit is contained in:
parent
af1093d4aa
commit
3e1b4bb788
2 changed files with 142 additions and 7 deletions
|
|
@ -99,7 +99,6 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
const spaceId = c.req.param("spaceId");
|
||||
const db = database(c.env.HYPERDRIVE.connectionString);
|
||||
|
||||
|
||||
const space = await db
|
||||
.select()
|
||||
.from(spaces)
|
||||
|
|
@ -261,7 +260,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
error instanceof Error &&
|
||||
error.message.includes("saved_spaces_user_space_idx")
|
||||
) {
|
||||
// Space is already favorited
|
||||
// Space is already favorited
|
||||
return c.json({ message: "Space already favorited" });
|
||||
}
|
||||
throw error;
|
||||
|
|
@ -634,7 +633,54 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
|
|||
|
||||
return c.json({ success: true });
|
||||
}
|
||||
).delete("/:spaceId", async (c) => {
|
||||
)
|
||||
.patch(
|
||||
"/:spaceId",
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
name: z.string().min(1, "Space name cannot be empty").max(100),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const user = c.get("user");
|
||||
if (!user) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const { spaceId } = c.req.param();
|
||||
const { name } = c.req.valid("json");
|
||||
const db = database(c.env.HYPERDRIVE.connectionString);
|
||||
|
||||
// Get space and verify ownership
|
||||
const space = await db
|
||||
.select()
|
||||
.from(spaces)
|
||||
.where(eq(spaces.uuid, spaceId))
|
||||
.limit(1);
|
||||
|
||||
if (space.length === 0) {
|
||||
return c.json({ error: "Space not found" }, 404);
|
||||
}
|
||||
|
||||
if (space[0].ownerId !== user.id) {
|
||||
return c.json({ error: "Only space owner can edit space name" }, 403);
|
||||
}
|
||||
|
||||
if (name.trim() === "<HOME>") {
|
||||
return c.json({ error: "Cannot use reserved name <HOME>" }, 400);
|
||||
}
|
||||
|
||||
// Update space name
|
||||
await db
|
||||
.update(spaces)
|
||||
.set({ name: name.trim() })
|
||||
.where(eq(spaces.uuid, spaceId));
|
||||
|
||||
return c.json({ success: true, name: name.trim() });
|
||||
}
|
||||
)
|
||||
.delete("/:spaceId", async (c) => {
|
||||
const user = c.get("user");
|
||||
if (!user) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LoaderFunctionArgs, json } from "@remix-run/cloudflare";
|
|||
import { useLoaderData, useNavigate } from "@remix-run/react";
|
||||
|
||||
import { getSessionFromRequest } from "@supermemory/authkit-remix-cloudflare/src/session";
|
||||
import { Clipboard, DeleteIcon, Share, Star, Trash, UserPlus } from "lucide-react";
|
||||
import { Clipboard, DeleteIcon, Pencil, Share, Star, Trash, UserPlus } from "lucide-react";
|
||||
import { proxy } from "server/proxy";
|
||||
import { toast } from "sonner";
|
||||
import Navbar from "~/components/Navbar";
|
||||
|
|
@ -76,6 +76,9 @@ export default function SpacePage() {
|
|||
const [accessType, setAccessType] = useState<"read" | "edit">("read");
|
||||
const [isInviting, setIsInviting] = useState(false);
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedName, setEditedName] = useState(space.name);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only update if we're on exactly /space/spaceid and not already in the correct format
|
||||
|
|
@ -95,6 +98,40 @@ export default function SpacePage() {
|
|||
}
|
||||
}, [space, navigate]);
|
||||
|
||||
const handleEditName = async () => {
|
||||
if (!editedName.trim() || editedName.trim() === space.name || isUpdating) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch(`/backend/v1/spaces/${space.uuid}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: editedName.trim() }),
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = (await response.json()) as { error: string };
|
||||
throw new Error(data.error || "Failed to update space name");
|
||||
}
|
||||
|
||||
toast.success("Space name updated successfully");
|
||||
// Update URL with new name
|
||||
const urlFriendlyName = editedName
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
navigate(`/space/${space.uuid}---${urlFriendlyName}`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Failed to update space name");
|
||||
setEditedName(space.name); // Reset to original name on error
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInvite = async () => {
|
||||
if (!email) return;
|
||||
|
||||
|
|
@ -189,9 +226,61 @@ export default function SpacePage() {
|
|||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-3 md:justify-between">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-3">
|
||||
<h1 className="font-geist text-3xl font-semibold dark:text-neutral-100 text-neutral-700 tracking-[-0.020em]">
|
||||
{space.name}
|
||||
</h1>
|
||||
{isEditing && space.permissions.isOwner ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={editedName}
|
||||
onChange={(e) => setEditedName(e.target.value)}
|
||||
className="text-3xl font-semibold h-auto py-1"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleEditName();
|
||||
} else if (e.key === "Escape") {
|
||||
setIsEditing(false);
|
||||
setEditedName(space.name);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditedName(space.name);
|
||||
}}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleEditName}
|
||||
disabled={
|
||||
isUpdating || !editedName.trim() || editedName.trim() === space.name
|
||||
}
|
||||
>
|
||||
{isUpdating ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="font-geist text-3xl font-semibold dark:text-neutral-100 text-neutral-700 tracking-[-0.020em]">
|
||||
{space.name}
|
||||
</h1>
|
||||
{space.permissions.isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{space.permissions.canEdit && (
|
||||
<span className="px-2 py-0.5 text-sm bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 rounded-md">
|
||||
Can Edit
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue