Skip to content

Commit 29cacd5

Browse files
committed
fix editing spaces and member count display
1 parent 0a53562 commit 29cacd5

File tree

5 files changed

+132
-78
lines changed

5 files changed

+132
-78
lines changed

apps/web/actions/organization/create-space.ts

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getCurrentUser } from "@cap/database/auth/session";
55
import { nanoId } from "@cap/database/helpers";
66
import { spaceMembers, spaces } from "@cap/database/schema";
77
import {
8+
type ImageUpload,
89
Space,
910
SpaceMemberId,
1011
type SpaceMemberRole,
@@ -65,21 +66,50 @@ export async function createSpace(
6566

6667
// Generate the space ID early so we can use it in the file path
6768
const spaceId = Space.SpaceId.make(nanoId());
69+
let iconUrl: ImageUpload.ImageUrlOrKey | null = null;
70+
71+
await db().transaction(async (tx) => {
72+
// Create the space first
73+
await tx.insert(spaces).values({
74+
id: spaceId,
75+
name,
76+
organizationId: user.activeOrganizationId,
77+
createdById: user.id,
78+
iconUrl: null,
79+
});
6880

69-
// Create the space first
70-
await db().insert(spaces).values({
71-
id: spaceId,
72-
name,
73-
organizationId: user.activeOrganizationId,
74-
createdById: user.id,
75-
iconUrl: null,
76-
createdAt: new Date(),
77-
updatedAt: new Date(),
81+
// --- Member Management Logic ---
82+
// Collect member user IDs from formData
83+
const memberUserIds: string[] = [];
84+
for (const entry of formData.getAll("members[]")) {
85+
if (typeof entry === "string" && entry.length > 0) {
86+
memberUserIds.push(entry);
87+
}
88+
}
89+
90+
// Always add the creator as Admin (if not already in the list)
91+
if (!memberUserIds.includes(user.id)) {
92+
memberUserIds.push(user.id);
93+
}
94+
95+
// Create space members
96+
if (memberUserIds.length > 0) {
97+
const spaceMembersToInsert = memberUserIds.map((userId) => {
98+
// Creator is always Admin, others are member
99+
const role: SpaceMemberRole = userId === user.id ? "Admin" : "member";
100+
return {
101+
id: SpaceMemberId.make(nanoId()),
102+
spaceId,
103+
userId: User.UserId.make(userId),
104+
role,
105+
};
106+
});
107+
108+
await tx.insert(spaceMembers).values(spaceMembersToInsert);
109+
}
78110
});
79111

80-
// Upload icon if provided
81112
const iconFile = formData.get("icon") as File | null;
82-
let iconUrl = null;
83113

84114
if (iconFile) {
85115
try {
@@ -89,50 +119,16 @@ export async function createSpace(
89119
iconUrl = result.iconUrl;
90120
} catch (error) {
91121
console.error("Error uploading space icon:", error);
92-
// Don't fail the space creation if icon upload fails
93-
// The space is already created, just without an icon
94-
}
95-
}
96-
97-
// --- Member Management Logic ---
98-
// Collect member user IDs from formData
99-
const memberUserIds: string[] = [];
100-
for (const entry of formData.getAll("members[]")) {
101-
if (typeof entry === "string" && entry.length > 0) {
102-
memberUserIds.push(entry);
103122
}
104123
}
105124

106-
// Always add the creator as Admin (if not already in the list)
107-
if (!memberUserIds.includes(user.id)) {
108-
memberUserIds.push(user.id);
109-
}
110-
111-
// Create space members
112-
if (memberUserIds.length > 0) {
113-
const spaceMembersToInsert = memberUserIds.map((userId) => {
114-
// Creator is always Admin, others are member
115-
const role: SpaceMemberRole = userId === user.id ? "Admin" : "member";
116-
return {
117-
id: SpaceMemberId.make(nanoId()),
118-
spaceId,
119-
userId: User.UserId.make(userId),
120-
role,
121-
createdAt: new Date(),
122-
updatedAt: new Date(),
123-
};
124-
});
125-
126-
await db().insert(spaceMembers).values(spaceMembersToInsert);
127-
}
128-
129125
revalidatePath("/dashboard");
130126

131127
return {
132128
success: true,
133129
spaceId,
134-
name,
135130
iconUrl,
131+
name,
136132
};
137133
} catch (error) {
138134
console.error("Error creating space:", error);

apps/web/actions/organization/update-space.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
5-
import { nanoIdLength } from "@cap/database/helpers";
5+
import { nanoId } from "@cap/database/helpers";
66
import { spaceMembers, spaces } from "@cap/database/schema";
77
import { S3Buckets } from "@cap/web-backend";
8-
import { Space, type User } from "@cap/web-domain";
8+
import {
9+
Space,
10+
SpaceMemberId,
11+
type SpaceMemberRole,
12+
type User,
13+
} from "@cap/web-domain";
914
import { and, eq } from "drizzle-orm";
1015
import { Effect, Option } from "effect";
1116
import { revalidatePath } from "next/cache";
12-
import { v4 as uuidv4 } from "uuid";
1317
import { runPromise } from "@/lib/server";
1418
import { uploadSpaceIcon } from "./upload-space-icon";
1519

@@ -22,40 +26,64 @@ export async function updateSpace(formData: FormData) {
2226
const members = formData.getAll("members[]") as User.UserId[];
2327
const iconFile = formData.get("icon") as File | null;
2428

29+
// Get the space to check authorization
30+
const [space] = await db()
31+
.select({
32+
createdById: spaces.createdById,
33+
organizationId: spaces.organizationId,
34+
})
35+
.from(spaces)
36+
.where(eq(spaces.id, id))
37+
.limit(1);
38+
39+
if (!space) {
40+
return { success: false, error: "Space not found" };
41+
}
42+
43+
// Check if user is the creator or a member of the space
44+
const isCreator = space.createdById === user.id;
2545
const [membership] = await db()
2646
.select()
2747
.from(spaceMembers)
28-
.where(and(eq(spaceMembers.spaceId, id), eq(spaceMembers.userId, user.id)));
48+
.where(and(eq(spaceMembers.spaceId, id), eq(spaceMembers.userId, user.id)))
49+
.limit(1);
2950

30-
if (!membership) return { success: false, error: "Unauthorized" };
51+
if (!isCreator && !membership) {
52+
return { success: false, error: "Unauthorized" };
53+
}
3154

3255
// Update space name
3356
await db().update(spaces).set({ name }).where(eq(spaces.id, id));
3457

35-
// Update members (simple replace for now)
58+
// Update members - ensure creator is always included
59+
const memberIds = Array.from(new Set([...members, space.createdById]));
60+
3661
await db().delete(spaceMembers).where(eq(spaceMembers.spaceId, id));
37-
if (members.length > 0) {
38-
await db()
39-
.insert(spaceMembers)
40-
.values(
41-
members.map((userId) => ({
42-
id: uuidv4().substring(0, nanoIdLength),
62+
await db()
63+
.insert(spaceMembers)
64+
.values(
65+
memberIds.map((userId) => {
66+
const role: SpaceMemberRole =
67+
userId === space.createdById ? "Admin" : "member";
68+
return {
69+
id: SpaceMemberId.make(nanoId()),
4370
spaceId: id,
4471
userId,
45-
})),
46-
);
47-
}
72+
role,
73+
};
74+
}),
75+
);
4876

4977
// Handle icon removal if requested
5078
if (formData.get("removeIcon") === "true") {
5179
// Remove icon from S3 and set iconUrl to null
5280
const spaceArr = await db().select().from(spaces).where(eq(spaces.id, id));
53-
const space = spaceArr[0];
54-
if (space?.iconUrl) {
81+
const spaceData = spaceArr[0];
82+
if (spaceData?.iconUrl) {
5583
// Extract the S3 key (it might already be a key or could be a legacy URL)
56-
const key = space.iconUrl.startsWith("organizations/")
57-
? space.iconUrl
58-
: space.iconUrl.match(/organizations\/.+/)?.[0];
84+
const key = spaceData.iconUrl.startsWith("organizations/")
85+
? spaceData.iconUrl
86+
: spaceData.iconUrl.match(/organizations\/.+/)?.[0];
5987

6088
if (key) {
6189
try {
@@ -74,5 +102,6 @@ export async function updateSpace(formData: FormData) {
74102
}
75103

76104
revalidatePath("/dashboard");
105+
revalidatePath(`/dashboard/spaces/${id}`);
77106
return { success: true };
78107
}

apps/web/app/(org)/dashboard/_components/Navbar/SpaceDialog.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ImageUpload } from "@cap/web-domain";
1818
import { faLayerGroup } from "@fortawesome/free-solid-svg-icons";
1919
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2020
import { zodResolver } from "@hookform/resolvers/zod";
21+
import { useRouter } from "next/navigation";
2122
import type React from "react";
2223
import { useEffect, useRef, useState } from "react";
2324
import { useForm } from "react-hook-form";
@@ -131,6 +132,7 @@ const formSchema = z.object({
131132

132133
export const NewSpaceForm: React.FC<NewSpaceFormProps> = (props) => {
133134
const { edit = false, space } = props;
135+
const router = useRouter();
134136

135137
const form = useForm<z.infer<typeof formSchema>>({
136138
resolver: zodResolver(formSchema),
@@ -203,11 +205,19 @@ export const NewSpaceForm: React.FC<NewSpaceFormProps> = (props) => {
203205
if (selectedFile === null && space.iconUrl) {
204206
formData.append("removeIcon", "true");
205207
}
206-
await updateSpace(formData);
208+
const result = await updateSpace(formData);
209+
if (!result.success) {
210+
throw new Error(result.error || "Failed to update space");
211+
}
207212
toast.success("Space updated successfully");
213+
router.refresh();
208214
} else {
209-
await createSpace(formData);
215+
const result = await createSpace(formData);
216+
if (!result.success) {
217+
throw new Error(result.error || "Failed to create space");
218+
}
210219
toast.success("Space created successfully");
220+
router.refresh();
211221
}
212222

213223
form.reset();

apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { db } from "@cap/database";
44
import { getCurrentUser } from "@cap/database/auth/session";
55
import { nanoIdLength } from "@cap/database/helpers";
6-
import { spaceMembers } from "@cap/database/schema";
6+
import { spaceMembers, spaces } from "@cap/database/schema";
77
import { Space, User } from "@cap/web-domain";
88
import { eq, inArray } from "drizzle-orm";
99
import { revalidatePath } from "next/cache";
@@ -170,25 +170,41 @@ export async function setSpaceMembers(
170170
}
171171
const { spaceId, userIds, role } = validation.data;
172172

173+
// Get the space creator to ensure they're always included
174+
const [space] = await db()
175+
.select({ createdById: spaces.createdById })
176+
.from(spaces)
177+
.where(eq(spaces.id, spaceId))
178+
.limit(1);
179+
180+
if (!space) {
181+
throw new Error("Space not found");
182+
}
183+
184+
// Ensure creator is always included in the member list
185+
const allMemberIds = Array.from(new Set([...userIds, space.createdById]));
186+
173187
// Remove all current members
174188
await db().delete(spaceMembers).where(eq(spaceMembers.spaceId, spaceId));
175189

176-
// Insert new members if any
177-
if (userIds.length > 0) {
178-
const now = new Date();
179-
const values = userIds.map((userId) => ({
190+
// Insert new members (always at least the creator)
191+
const now = new Date();
192+
const values = allMemberIds.map((userId) => {
193+
// Creator is always Admin, others get the specified role
194+
const memberRole = userId === space.createdById ? "Admin" : role;
195+
return {
180196
id: User.UserId.make(uuidv4().substring(0, nanoIdLength)),
181197
spaceId,
182198
userId,
183-
role,
199+
role: memberRole,
184200
createdAt: now,
185201
updatedAt: now,
186-
}));
187-
await db().insert(spaceMembers).values(values);
188-
}
202+
};
203+
});
204+
await db().insert(spaceMembers).values(values);
189205

190206
revalidatePath(`/dashboard/spaces/${spaceId}`);
191-
return { success: true, count: userIds.length };
207+
return { success: true, count: allMemberIds.length };
192208
}
193209

194210
const batchRemoveSpaceMembersSchema = z.object({

apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersIndicator.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { type Space, User } from "@cap/web-domain";
1616
import { faPlus, faUserGroup } from "@fortawesome/free-solid-svg-icons";
1717
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1818
import { zodResolver } from "@hookform/resolvers/zod";
19+
import { useRouter } from "next/navigation";
1920
import { useCallback, useState } from "react";
2021
import { useForm } from "react-hook-form";
2122
import { toast } from "sonner";
@@ -44,6 +45,7 @@ export const MembersIndicator = ({
4445
onAddVideos,
4546
}: MembersIndicatorProps) => {
4647
const { user } = useDashboardContext();
48+
const router = useRouter();
4749
const [open, setOpen] = useState(false);
4850
const [isLoading, setIsLoading] = useState(false);
4951

@@ -81,6 +83,7 @@ export const MembersIndicator = ({
8183
role: "member",
8284
});
8385
toast.success("Members updated!");
86+
router.refresh();
8487
} catch (error) {
8588
console.error("Failed to update members:", error);
8689
toast.error("Failed to update members");

0 commit comments

Comments
 (0)