Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/app/(dashboard)/projects/[projectId]/users/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default function ProjectUsersPage() {
const { projectId } = useParams();
const [users, setUsers] = useState<UserColumn[]>([]);
const [projectData, setProjectData] = useState<ProjectResponse[]>([]);
const [projectName, setProjectName] = useState<string>("");
const [originatingProjectId, setOriginatingProjectId] = useState<
number | null
>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
Expand All @@ -31,6 +35,9 @@ export default function ProjectUsersPage() {
getProjects(),
]);

const projectIdNum = parseInt(projectId as string, 10);
setOriginatingProjectId(projectIdNum);

if (usersResult.success) {
setUsers(usersResult.users);
} else {
Expand All @@ -39,6 +46,11 @@ export default function ProjectUsersPage() {

if (projectsResult.success) {
setProjectData(projectsResult.projects);

const projectName = projectsResult.projects.find(
(project) => project.id === projectIdNum,
)?.name;
setProjectName(projectName);
} else {
toast.error("Error", {
description: `Failed to fetch projects: ${projectsResult.error}`,
Expand Down Expand Up @@ -85,11 +97,7 @@ export default function ProjectUsersPage() {
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/projects/${projectId}`}>
{
projectData.find(
(project) => project.id == (projectId as unknown),
)?.name
}
{projectName ? projectName : "Project"}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
Expand All @@ -108,6 +116,9 @@ export default function ProjectUsersPage() {
}))}
isLoading={loading}
onUserAction={handleUserAction}
originatingProjectId={
originatingProjectId != null ? originatingProjectId : undefined
}
/>
</div>
);
Expand Down
56 changes: 53 additions & 3 deletions src/components/forms/AddUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
import { Input } from "../ui/input";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { createUserAction } from "@/lib/actions";
import { createUserAction, linkUserToProjectId } from "@/lib/actions";
import { UserColumn } from "../usertable/columns";
import { CircleX, Loader2 } from "lucide-react";
import { useState } from "react";
import { Alert } from "../ui/alert";
import { toast } from "sonner";

const createUserSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
Expand All @@ -27,9 +28,14 @@ const createUserSchema = z.object({
type CreateUserFormProps = {
onUserAdd: (newUser: UserColumn) => void;
onClose?: () => void;
projectIds?: number[];
};

const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => {
const CreateUserForm = ({
onUserAdd,
onClose,
projectIds,
}: CreateUserFormProps) => {
/** Form to create a user */
const createUserForm = useForm({
resolver: zodResolver(createUserSchema),
Expand Down Expand Up @@ -57,6 +63,43 @@ const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => {
role: result.user.type,
projects: result.user.projectIds,
};

// Link user to project if originatingProjectId is provided
if (projectIds && projectIds.length > 0) {
const projectPromises: Promise<{ success: boolean }>[] = [];

for (const projectId of projectIds) {
projectPromises.push(
linkUserToProjectId({
projectId: projectId,
userId: String(result.user.id),
}),
);
}

const linkResults = await Promise.all(projectPromises);
const failedLinks = linkResults.filter((r) => !r.success);

if (!newUser.projects) {
newUser.projects = [];
}

for (let i = 0; i < linkResults.length; i++) {
if (linkResults[i].success) {
newUser.projects.push(projectIds[i]);
}
}

newUser.projects = Array.from(new Set(newUser.projects));

// Show warning if any links failed
if (failedLinks.length > 0) {
toast.warning("User created with incomplete project links", {
description: `User was created successfully, but failed to link to ${failedLinks.length} project${failedLinks.length > 1 ? "s" : ""}. Please manually add them to the project.`,
});
}
}

if (onUserAdd) {
onUserAdd(newUser);
}
Expand All @@ -68,7 +111,14 @@ const CreateUserForm = ({ onUserAdd, onClose }: CreateUserFormProps) => {
setLoading(false);
}
} catch (error) {
console.error("Error creating user:", error);
setError("An unexpected error occurred. Please try again.");
toast.error("Failed to create user", {
description:
error instanceof Error
? error.message
: "An unexpected error occurred",
});
setLoading(false);
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const badgeVariants = cva(
);

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const buttonVariants = cva(
);

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const sheetVariants = cva(
);

interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
extends
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}

const SheetContent = React.forwardRef<
Expand Down
7 changes: 7 additions & 0 deletions src/components/usertable/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ interface DataTableProps<TData> {
projectData: ProjectColumn[];
isLoading: boolean;
onUserAction: (user: UserColumn, action: "add" | "update" | "delete") => void;
originatingProjectId?: number;
}

export function UserDataTable<TData>({
data,
projectData,
isLoading,
onUserAction,
originatingProjectId,
}: DataTableProps<TData>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false);
Expand Down Expand Up @@ -153,6 +155,11 @@ export function UserDataTable<TData>({
<CreateUserForm
onUserAdd={(user) => onUserAction?.(user, "add")}
onClose={() => setIsAddUserDialogOpen(false)}
projectIds={
originatingProjectId != undefined
? [originatingProjectId]
: []
}
/>
</DialogContent>
</Dialog>
Expand Down
30 changes: 30 additions & 0 deletions src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,36 @@ export async function linkUserToProject(data: {
}
}

export async function linkUserToProjectId(data: {
projectId: number;
userId: string;
}) {
const session = await getSession();
if (!session) {
return { success: false, error: "Unauthorized" };
}

if (!requireAdmin(session.user)) {
return {
success: false,
error: "Only admins and superadmins can link users to projects",
};
}

const junoClient = getJunoInstance();
try {
await junoClient.user.linkToProject({
credentials: session.jwt,
project: { id: data.projectId },
userId: data.userId,
});
return { success: true };
} catch (error) {
console.error("Error linking user:", error);
Comment thread
llam36 marked this conversation as resolved.
return { success: false, error: "Failed to link user type to project" };
}
}

export async function unlinkUserFromProject(data: {
projectName: string;
userId: string;
Expand Down
Loading