Fixes all 542 ESLint problems in the web package to achieve 0 errors and 0 warnings. Changes: - Fixed 144 issues: nullish coalescing, return types, unused variables - Fixed 118 issues: unnecessary conditions, type safety, template literals - Fixed 79 issues: non-null assertions, unsafe assignments, empty functions - Fixed 67 issues: explicit return types, promise handling, enum comparisons - Fixed 45 final warnings: missing return types, optional chains - Fixed 25 typecheck-related issues: async/await, type assertions, formatting - Fixed JSX.Element namespace errors across 90+ files All Quality Rails violations resolved. Lint and typecheck both pass with 0 problems. Files modified: 118 components, tests, hooks, and utilities Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
136 lines
5.2 KiB
TypeScript
136 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import type { User, WorkspaceMember } from "@mosaic/shared";
|
|
import { WorkspaceMemberRole } from "@mosaic/shared";
|
|
|
|
export interface WorkspaceMemberWithUser extends WorkspaceMember {
|
|
user: User;
|
|
}
|
|
|
|
interface MemberListProps {
|
|
members: WorkspaceMemberWithUser[];
|
|
currentUserId: string;
|
|
currentUserRole: WorkspaceMemberRole;
|
|
workspaceOwnerId: string;
|
|
onRoleChange: (userId: string, newRole: WorkspaceMemberRole) => Promise<void>;
|
|
onRemove: (userId: string) => Promise<void>;
|
|
}
|
|
|
|
const roleColors: Record<WorkspaceMemberRole, string> = {
|
|
[WorkspaceMemberRole.OWNER]: "bg-purple-100 text-purple-700",
|
|
[WorkspaceMemberRole.ADMIN]: "bg-blue-100 text-blue-700",
|
|
[WorkspaceMemberRole.MEMBER]: "bg-green-100 text-green-700",
|
|
[WorkspaceMemberRole.GUEST]: "bg-gray-100 text-gray-700",
|
|
};
|
|
|
|
export function MemberList({
|
|
members,
|
|
currentUserId,
|
|
currentUserRole,
|
|
workspaceOwnerId,
|
|
onRoleChange,
|
|
onRemove,
|
|
}: MemberListProps): React.JSX.Element {
|
|
const canManageMembers =
|
|
currentUserRole === WorkspaceMemberRole.OWNER || currentUserRole === WorkspaceMemberRole.ADMIN;
|
|
|
|
const handleRoleChange = async (userId: string, newRole: WorkspaceMemberRole): Promise<void> => {
|
|
try {
|
|
await onRoleChange(userId, newRole);
|
|
} catch (error) {
|
|
console.error("Failed to change role:", error);
|
|
alert("Failed to change member role");
|
|
}
|
|
};
|
|
|
|
const handleRemove = async (userId: string): Promise<void> => {
|
|
if (!confirm("Are you sure you want to remove this member?")) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await onRemove(userId);
|
|
} catch (error) {
|
|
console.error("Failed to remove member:", error);
|
|
alert("Failed to remove member");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Members ({members.length})</h2>
|
|
<ul className="divide-y divide-gray-200">
|
|
{members.map((member) => {
|
|
const isCurrentUser = member.userId === currentUserId;
|
|
const isOwner = member.userId === workspaceOwnerId;
|
|
const canModify = canManageMembers && !isOwner && !isCurrentUser;
|
|
|
|
return (
|
|
<li key={member.userId} className="py-4 first:pt-0 last:pb-0">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
|
{member.user.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<p className="font-medium text-gray-900">{member.user.name}</p>
|
|
{isCurrentUser && <span className="text-xs text-gray-500">(you)</span>}
|
|
</div>
|
|
<p className="text-sm text-gray-600">{member.user.email}</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Joined {new Date(member.joinedAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{canModify ? (
|
|
<select
|
|
value={member.role}
|
|
onChange={(e) =>
|
|
handleRoleChange(member.userId, e.target.value as WorkspaceMemberRole)
|
|
}
|
|
className="px-3 py-1 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
<option value={WorkspaceMemberRole.ADMIN}>Admin</option>
|
|
<option value={WorkspaceMemberRole.MEMBER}>Member</option>
|
|
<option value={WorkspaceMemberRole.GUEST}>Guest</option>
|
|
</select>
|
|
) : (
|
|
<span
|
|
className={`px-3 py-1 rounded-full text-xs font-medium ${roleColors[member.role]}`}
|
|
>
|
|
{member.role}
|
|
</span>
|
|
)}
|
|
{canModify && (
|
|
<button
|
|
onClick={() => handleRemove(member.userId)}
|
|
className="text-red-600 hover:text-red-700 text-sm"
|
|
aria-label="Remove member"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
);
|
|
}
|