Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .cursor/rules/react-code.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ When writing React code follow these standards:
- Always name files that export components in PascalCase
- Always round corners to rounded-sm
- Always try to keep components small and modular
- Always use sonner instead of toast.
- Always use sonner instead of toast.
10 changes: 10 additions & 0 deletions .cursor/rules/typescript-rules.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description:
globs: *.ts*
alwaysApply: true
---
When writing TypeScript code follow these standards:

- Always use TypeScript
- NEVER use the `any` type as all costs.
- Try as much as possible to rely on TypeScript's inference instead of explicit type annotations.
6 changes: 5 additions & 1 deletion .github/workflows/auto-pr-to-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ on:
push:
branches:
- feature/*
- feat/*
- chore/*
- patch/*
- fix/*
- bug/*
- task/*
- story/*
Expand Down Expand Up @@ -33,4 +37,4 @@ jobs:
pr_label: "automated-pr"
pr_body: |
This is an automated pull request to merge ${{ github.ref_name }} into dev.
It was created by the [Auto Pull Request] action.
It was created by the [Auto Pull Request] action.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Edit, MoreHorizontal, Trash2 } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useState } from "react";
import React, { useState, useRef } from "react";

import {
AlertDialog,
Expand All @@ -20,17 +20,19 @@ import { Badge } from "@comp/ui/badge";
import { Button } from "@comp/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@comp/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@comp/ui/dropdown-menu";
import { Label } from "@comp/ui/label";
import type { Role } from "@prisma/client";
Expand Down Expand Up @@ -62,13 +64,17 @@ function getInitials(name?: string | null, email?: string | null): string {
export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
const params = useParams<{ orgId: string }>();
const { orgId } = params;
const [isRoleDialogOpen, setIsRoleDialogOpen] = useState(false);

const [isRemoveAlertOpen, setIsRemoveAlertOpen] = useState(false);
const [isUpdateRolesOpen, setIsUpdateRolesOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Role[]>(
Array.isArray(member.role) ? member.role : ([member.role] as Role[]),
);
const [isUpdating, setIsUpdating] = useState(false);
const [isRemoving, setIsRemoving] = useState(false);
const dropdownTriggerRef = useRef<HTMLButtonElement>(null);
const focusRef = useRef<HTMLButtonElement | null>(null);
const currentUserIsOwner = member.role === "owner";

const memberName = member.user.name || member.user.email || "Member";
Expand All @@ -89,7 +95,19 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {

const isEmployee = currentRoles.includes("employee");

const handleDialogItemSelect = () => {
focusRef.current = dropdownTriggerRef.current;
};

const handleDialogOpenChange = (open: boolean) => {
setIsUpdateRolesOpen(open);
if (open === false) {
setDropdownOpen(false);
}
};

const handleUpdateRolesClick = async () => {
console.log("handleUpdateRolesClick");
let rolesToUpdate = selectedRoles;
if (isOwner && !rolesToUpdate.includes("owner")) {
rolesToUpdate = [...rolesToUpdate, "owner"];
Expand All @@ -103,7 +121,7 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
setIsUpdating(true);
await onUpdateRole(memberId, rolesToUpdate);
setIsUpdating(false);
setIsRoleDialogOpen(false);
setIsUpdateRolesOpen(false); // Close dialog after update
};

const handleRemoveClick = async () => {
Expand Down Expand Up @@ -136,19 +154,13 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
</Link>
)}
</div>
<div className="text-sm text-muted-foreground">
{memberEmail}
</div>
<div className="text-sm text-muted-foreground">{memberEmail}</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap gap-1 justify-end max-w-[150px]">
{currentRoles.map((role) => (
<Badge
key={role}
variant="secondary"
className="text-xs"
>
<Badge key={role} variant="secondary" className="text-xs">
{(() => {
switch (role) {
case "owner":
Expand All @@ -167,9 +179,10 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
))}
</div>

<DropdownMenu>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button
ref={dropdownTriggerRef}
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
Expand All @@ -178,101 +191,101 @@ export function MemberRow({ member, onRemove, onUpdateRole }: MemberRowProps) {
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
align="end"
hidden={isUpdateRolesOpen}
onCloseAutoFocus={(event) => {
if (focusRef.current) {
focusRef.current.focus();
focusRef.current = null;
event.preventDefault();
}
}}
>
{canEditRoles && (
<DropdownMenuItem
onSelect={() => setIsRoleDialogOpen(true)}
<Dialog
open={isUpdateRolesOpen}
onOpenChange={handleDialogOpenChange}
>
<Edit className="mr-2 h-4 w-4" />
<span>
{"Edit Roles"}
</span>
</DropdownMenuItem>
<DialogTrigger asChild>
<DropdownMenuItem
onSelect={(event) => {
event.preventDefault();
handleDialogItemSelect();
}}
>
<Edit className="mr-2 h-4 w-4" />
<span>{"Edit Roles"}</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{"Edit Member Roles"}</DialogTitle>
<DialogDescription>
{"Change roles for"} {memberName}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor={`role-${memberId}`}>{"Roles"}</Label>
<MultiRoleCombobox
selectedRoles={selectedRoles}
onSelectedRolesChange={setSelectedRoles}
placeholder={"Select a role"}
lockedRoles={isOwner ? ["owner"] : []}
/>
{isOwner && (
<p className="text-xs text-muted-foreground mt-1">
{"The owner role cannot be removed."}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
{"Members must have at least one role."}
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsUpdateRolesOpen(false)}
>
Cancel
</Button>
<Button
onClick={handleUpdateRolesClick}
disabled={isUpdating || selectedRoles.length === 0}
>
{"Update"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{canRemove && (
<DropdownMenuItem
className="text-destructive focus:text-destructive focus:bg-destructive/10"
onSelect={() => setIsRemoveAlertOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>
{"Remove Member"}
</span>
<span>{"Remove Member"}</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

<Dialog open={isRoleDialogOpen} onOpenChange={setIsRoleDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{"Edit Member Roles"}
</DialogTitle>
<DialogDescription>
{"Change roles for"}{" "}
{memberName}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor={`role-${memberId}`}>
{"Roles"}
</Label>
<MultiRoleCombobox
selectedRoles={selectedRoles}
onSelectedRolesChange={setSelectedRoles}
placeholder={"Select a role"}
lockedRoles={isOwner ? ["owner"] : []}
/>
{isOwner && (
<p className="text-xs text-muted-foreground mt-1">
{"The owner role cannot be removed."}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
{"Members must have at least one role."}
</p>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsRoleDialogOpen(false)}
>
{"Cancel"}
</Button>
<Button
onClick={handleUpdateRolesClick}
disabled={isUpdating || selectedRoles.length === 0}
>
{"Update"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

<AlertDialog
open={isRemoveAlertOpen}
onOpenChange={setIsRemoveAlertOpen}
>
<AlertDialog open={isRemoveAlertOpen} onOpenChange={setIsRemoveAlertOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{"Remove Team Member"}
</AlertDialogTitle>
<AlertDialogTitle>{"Remove Team Member"}</AlertDialogTitle>
<AlertDialogDescription>
{"Are you sure you want to remove"}{" "}
{memberName}?{" "}
{"Are you sure you want to remove"} {memberName}?{" "}
{"They will no longer have access to this organization."}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{"Cancel"}
</AlertDialogCancel>
<AlertDialogCancel>{"Cancel"}</AlertDialogCancel>
<AlertDialogAction
onClick={handleRemoveClick}
disabled={isRemoving}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ export function MultiRoleCombobox({

// Filter out owner role for non-owners
const availableRoles = React.useMemo(() => {
return selectableRoles.filter(
(role) => role.value !== "owner" || isOwner,
);
return selectableRoles.filter((role) => role.value !== "owner" || isOwner);
}, [isOwner]);

const handleSelect = (roleValue: Role) => {
Expand All @@ -78,10 +76,7 @@ export function MultiRoleCombobox({
}

// If the role is locked, don't allow deselection
if (
lockedRoles.includes(roleValue) &&
selectedRoles.includes(roleValue)
) {
if (lockedRoles.includes(roleValue) && selectedRoles.includes(roleValue)) {
return; // Don't allow deselection of locked roles
}

Expand Down
Loading