Skip to content

Commit

Permalink
feat!(frontend and backend): User can now view, add, and remove works…
Browse files Browse the repository at this point in the history
…pace memebers successfully - still a couple of minor bugs to fix - but the functionalisty works. [2024-11-23]

BREAKING CHANGE: User can now view, add, and remove workspace memebers successfully - still a couple of minor bugs to fix - but the functionalisty works.
  • Loading branch information
CHRISCARLON committed Nov 23, 2024
1 parent 835a6d5 commit 2322e39
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 14 deletions.
4 changes: 3 additions & 1 deletion gridwalk-backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
// Initialise tracing
tracing_subscriber::fmt::init();

// Create DynamoDB client
let table_name = env::var("DYNAMODB_TABLE").unwrap_or_else(|_| "gridwalk".to_string());
let app_db = Dynamodb::new(false, &table_name).await.unwrap();
// FOR LOCAL DB DEV
// let app_db = Dynamodb::new(true, "gridwalk").await.unwrap();

// Create GeospatialConfig
let geospatial_config = GeospatialConfig::new();
Expand Down
8 changes: 7 additions & 1 deletion gridwalk-backend/src/routes/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ pub struct ReqAddWorkspaceMember {
role: WorkspaceRole,
}

#[derive(Debug, Deserialize)]
pub struct ReqRemoveWorkspaceMember {
workspace_id: String,
email: String,
}

#[derive(Debug, Serialize)]
pub struct SimpleMemberResponse {
role: WorkspaceRole,
Expand Down Expand Up @@ -107,7 +113,7 @@ pub async fn add_workspace_member(
pub async fn remove_workspace_member(
State(state): State<Arc<AppState>>,
Extension(auth_user): Extension<AuthUser>,
Json(req): Json<ReqAddWorkspaceMember>,
Json(req): Json<ReqRemoveWorkspaceMember>,
) -> Response {
if let Some(req_user) = auth_user.user {
// Get the workspace
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server";

import { getAuthToken } from "../lib/auth";
import { revalidatePath } from "next/cache";

type WorkspaceMember = {
email: string;
Expand Down Expand Up @@ -42,9 +42,6 @@ export async function getWorkspaceMembers(

const members: WorkspaceMember[] = await response.json();

// Revalidate the members list page/cache
revalidatePath(`/workspaces/${workspaceId}/members`);

return members;
} catch (error) {
console.error("Failed to fetch workspace members:", error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./members";
export * from "./get_members";
export * from "./remove_members";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use server";
import { getAuthToken } from "../lib/auth";

interface RemoveWorkspaceMemberRequest {
workspace_id: string;
email: string;
}

export async function removeWorkspaceMember(
data: RemoveWorkspaceMemberRequest,
): Promise<void> {
const token = await getAuthToken();

// Validation
if (!data.workspace_id) throw new Error("Workspace ID is required");
if (!data.email) throw new Error("Email is required");

try {
const response = await fetch(
`${process.env.GRIDWALK_API}/workspace/${data.workspace_id}/members/${data.email}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
workspace_id: data.workspace_id,
email: data.email,
}),
},
);

if (!response.ok) {
const errorText = await response.text();
if (response.status === 401) throw new Error("Authentication failed");
throw new Error(errorText || "Failed to remove workspace member");
}
} catch (error) {
console.error("Failed to remove workspace member:", error);
throw error;
}
}
52 changes: 48 additions & 4 deletions gridwalk-ui/src/app/workspace/[workspaceId]/viewMembersModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";
import React, { useEffect, useState } from "react";
import { getWorkspaceMembers } from "./actions/workspace/get_members";
import { Shield, Mail, Loader2 } from "lucide-react";
import { Shield, Mail, Loader2, X } from "lucide-react";
import { removeWorkspaceMember } from "./actions/workspace/remove_members";
import { useRouter } from "next/navigation";

interface ViewWorkspaceMemberModalProps {
isOpen: boolean;
Expand All @@ -11,7 +13,7 @@ interface ViewWorkspaceMemberModalProps {

type WorkspaceMember = {
email: string;
role: "Admin" | "Read"; // Corrected to match your types
role: "Admin" | "Read";
};

export const ViewWorkspaceMemberModal: React.FC<
Expand All @@ -20,6 +22,8 @@ export const ViewWorkspaceMemberModal: React.FC<
const [members, setMembers] = useState<WorkspaceMember[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [removing, setRemoving] = useState<string | null>(null);
const router = useRouter();

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -53,6 +57,22 @@ export const ViewWorkspaceMemberModal: React.FC<
}
};

const handleRemoveMember = async (email: string) => {
try {
setRemoving(email);
await removeWorkspaceMember({
workspace_id: workspaceId,
email: email,
});
setMembers(members.filter((member) => member.email !== email));
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to remove member");
} finally {
setRemoving(null);
}
};

return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl relative">
Expand Down Expand Up @@ -85,9 +105,12 @@ export const ViewWorkspaceMemberModal: React.FC<
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider text-center">
Remove
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
Expand All @@ -105,12 +128,33 @@ export const ViewWorkspaceMemberModal: React.FC<
<div className="flex items-center">
<Shield className="h-4 w-4 text-gray-400 mr-2" />
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getRoleColor(member.role)}`}
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getRoleColor(
member.role,
)}`}
>
{member.role}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<button
onClick={() => handleRemoveMember(member.email)}
disabled={removing === member.email}
className={`inline-flex items-center justify-center h-8 w-8 rounded-full
${
removing === member.email
? "bg-gray-100 cursor-not-allowed"
: "text-red-600 hover:bg-red-100 transition-colors"
}`}
title="Remove member"
>
{removing === member.email ? (
<Loader2 className="h-4 w-4 animate-spin text-gray-500" />
) : (
<X className="h-4 w-4" />
)}
</button>
</td>
</tr>
))}
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ export default function WorkspaceProjectsClient({
<div className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 sm:gap-6">
<div className="space-y-2">
<h1 className="sm:text-4xl font-bold text-gray-900">Projects</h1>
<p className="text-gray-600">
Current Workspace: {currentWorkspace?.name || "Loading..."}
</p>
<h1 className="sm:text-4xl font-bold">
<span className="text-orange-400">Current Workspace:</span>{" "}
<span className="text-gray-900">
{currentWorkspace?.name || "Loading..."}
</span>
</h1>
</div>
<div className="flex flex-col sm:flex-row gap-2">
<button
Expand Down

0 comments on commit 2322e39

Please sign in to comment.