Implement collaboration features with user access management and email invitation functionality

This commit is contained in:
2025-06-25 09:35:47 +05:30
parent 17ec72e283
commit 13a2648e83
8 changed files with 307 additions and 83 deletions

View File

@@ -7,15 +7,27 @@ import { access } from "fs";
import MultiEmailInvite from "../ui/inputs/MultiEmailInvite"; import MultiEmailInvite from "../ui/inputs/MultiEmailInvite";
import { useActiveUsers } from "../../store/builder/store"; import { useActiveUsers } from "../../store/builder/store";
import { getUserData } from "../../functions/getUserData"; import { getUserData } from "../../functions/getUserData";
import { getProjectSharedList } from "../../services/factoryBuilder/collab/getProjectSharedList";
import { useParams } from "react-router-dom";
import { projection } from "@turf/turf";
import { shareAccess } from "../../services/factoryBuilder/collab/shareAccess";
import { getAvatarColor } from "../../modules/collaboration/functions/getAvatarColor";
interface UserListTemplateProps { interface UserListTemplateProps {
user: User; user: User;
} }
interface UserData {
_id: string;
Email: string;
userName: string
}
const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => { const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => {
const [accessSelection, setAccessSelection] = useState<string>(user.access); const [accessSelection, setAccessSelection] = useState<string>(user?.Access);
const { projectId } = useParams()
function accessUpdate({ option }: AccessOption) { const accessUpdate = async ({ option }: AccessOption) => {
if (!projectId) return
const accessSelection = await shareAccess(projectId, user.userId, option)
console.log('accessSelection: ', accessSelection);
setAccessSelection(option); setAccessSelection(option);
} }
@@ -26,18 +38,22 @@ const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => {
{user.profileImage ? ( {user.profileImage ? (
<img <img
src={user.profileImage || "https://via.placeholder.com/150"} src={user.profileImage || "https://via.placeholder.com/150"}
alt={`${user.name}'s profile`} alt={`${user.
userName}'s profile`}
/> />
) : ( ) : (
<div <div
className="no-profile-container" className="no-profile-container"
style={{ background: user.color }} style={{ background: getAvatarColor(1, user.userId) }}
> >
{user.name[0]} {user.
userName.charAt(0).toUpperCase()}
</div> </div>
)} )}
</div> </div>
<div className="user-name">{user.name}</div> <div className="user-name"> {user.
userName.charAt(0).toUpperCase() + user.
userName.slice(1).toLowerCase()}</div>
</div> </div>
<div className="user-access"> <div className="user-access">
<RegularDropDown <RegularDropDown
@@ -61,39 +77,27 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
}) => { }) => {
const { activeUsers } = useActiveUsers(); const { activeUsers } = useActiveUsers();
const { userName } = getUserData(); const { userName } = getUserData();
const [users, setUsers] = useState([])
const { projectId } = useParams();
const [searchedEmail, setSearchedEmail] = useState<UserData[]>([]);
const [emails, setEmails] = useState<any>([]);
function getData() {
if (!projectId) return;
getProjectSharedList(projectId).then((allUser) => {
const accesMail = allUser?.datas || []
setUsers(accesMail)
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
getData();
}, [])
useEffect(() => { useEffect(() => {
// console.log("activeUsers: ", activeUsers); // console.log("activeUsers: ", activeUsers);
}, [activeUsers]); }, [activeUsers]);
const users = [
{
name: "Alice Johnson",
email: "alice.johnson@example.com",
profileImage: "",
color: "#FF6600",
access: "Admin",
},
{
name: "Bob Smith",
email: "bob.smith@example.com",
profileImage: "",
color: "#488EF6",
access: "Viewer",
},
{
name: "Charlie Brown",
email: "charlie.brown@example.com",
profileImage: "",
color: "#48AC2A",
access: "Viewer",
},
{
name: "Diana Prince",
email: "diana.prince@example.com",
profileImage: "",
color: "#D44242",
access: "Viewer",
},
];
return ( return (
<RenderOverlay> <RenderOverlay>
<div <div
@@ -124,7 +128,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
</div> </div>
</div> </div>
<div className="invite-input-container"> <div className="invite-input-container">
<MultiEmailInvite /> <MultiEmailInvite setSearchedEmail={setSearchedEmail} searchedEmail={searchedEmail} users={users} getData={getData} />
</div> </div>
<div className="split"></div> <div className="split"></div>
<section> <section>

View File

@@ -1,72 +1,143 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { getSearchUsers } from "../../../services/factoryBuilder/collab/getSearchUsers";
import { useParams } from "react-router-dom";
import { shareProject } from "../../../services/factoryBuilder/collab/shareProject";
import { getUserData } from "../../../functions/getUserData";
const MultiEmailInvite: React.FC = () => { interface UserData {
const [emails, setEmails] = useState<string[]>([]); _id: string;
Email: string;
userName: string;
}
interface MultiEmailProps {
searchedEmail: UserData[];
setSearchedEmail: React.Dispatch<React.SetStateAction<UserData[]>>
users: any,
getData: any,
}
const MultiEmailInvite: React.FC<MultiEmailProps> = ({ searchedEmail, setSearchedEmail, users, getData }) => {
console.log('users: ', users);
const [emails, setEmails] = useState<any>([]);
const [inputFocus, setInputFocus] = useState(false);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const { projectId } = useParams();
const { userId } = getUserData();
const handleAddEmail = () => { const handleAddEmail = async (selectedUser: UserData) => {
if (!projectId || !selectedUser) return
const trimmedEmail = inputValue.trim(); const trimmedEmail = inputValue.trim();
setEmails((prev: any[]) => {
if (!selectedUser) return prev;
const isNotCurrentUser = selectedUser._id !== userId;
const alreadyExistsInEmails = prev.some(email => email._id === selectedUser._id);
const alreadyExistsInUsers = users.some((val: any) => val.userId === selectedUser._id);
// Validate email console.log('alreadyExistsInEmails: ', alreadyExistsInEmails);
if (!trimmedEmail || !validateEmail(trimmedEmail)) { console.log('alreadyExistsInUsers:', alreadyExistsInUsers);
alert("Please enter a valid email address.");
return;
}
// Check for duplicates if (isNotCurrentUser && !alreadyExistsInEmails && !alreadyExistsInUsers) {
if (emails.includes(trimmedEmail)) { return [...prev, selectedUser];
alert("This email has already been added."); }
return;
}
// Add email to the list return prev;
setEmails([...emails, trimmedEmail]); });
setInputValue(""); // Clear the input field after adding setInputValue(""); // Clear the input field after adding
}; };
const handleSearchMail = async (e: any) => {
setInputValue(e.target.value);
const trimmedEmail = e.target.value.trim();
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (trimmedEmail.length < 3) return;
if (e.key === "Enter" || e.key === ",") { try {
e.preventDefault(); const searchedMail = await getSearchUsers(trimmedEmail);
handleAddEmail(); console.log('searchedMail: ', searchedMail);
const filteredEmail = searchedMail.sharchMail?.filtered;
console.log('filteredEmail: ', filteredEmail);
if (filteredEmail) {
setSearchedEmail(filteredEmail)
}
} catch (error) {
console.error("Failed to search mail:", error);
} }
}; };
const handleRemoveEmail = (emailToRemove: string) => {
setEmails(emails.filter((email) => email !== emailToRemove)); const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "," && searchedEmail.length > 0) {
e.preventDefault();
handleAddEmail(searchedEmail[0]);
}
}; };
const handleRemoveEmail = (idToRemove: string) => {
setEmails((prev: any) => prev.filter((email: any) => email._id !== idToRemove));
};
const validateEmail = (email: string) => { const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email); return emailRegex.test(email);
}; };
const [inputFocus, setInputFocus] = useState(false); const handleInvite = () => {
if (!projectId) return;
try {
emails.forEach((user: any) => {
shareProject(user._id, projectId)
.then((res) => {
console.log("sharedProject:", res);
})
.catch((err) => {
console.error("Error sharing project:", err);
});
setEmails([])
});
setTimeout(() => {
getData()
}, 1000);
} catch (error) {
console.error("General error:", error);
}
};
return ( return (
<div className="multi-email-invite-input-container"> <div className="multi-email-invite-input-container">
<div className={`multi-email-invite-input${inputFocus ? " active" : ""}`}> <div className={`multi-email-invite-input${inputFocus ? " active" : ""}`}>
{emails.map((email, index) => ( {emails.map((email: any, index: number) => (
<div key={index} className="entered-emails"> <div key={index} className="entered-emails">
{email} {email.Email}
<span onClick={() => handleRemoveEmail(email)}>&times;</span> <span onClick={() => handleRemoveEmail(email._id)}>&times;</span>
</div> </div>
))} ))}
<input <input
type="text" type="text"
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => handleSearchMail(e)}
onFocus={() => setInputFocus(true)} onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)} // onBlur={() => setInputFocus(false)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Enter email and press Enter or comma to seperate" placeholder="Enter email and press Enter or comma to seperate"
/> />
</div> </div>
<div onClick={handleAddEmail} className="invite-button"> <div onClick={handleInvite} className="invite-button">
Invite Invite
</div> </div>
<div className="users-list-container"> {inputFocus && inputValue.length > 0 && searchedEmail && searchedEmail.length > 0 && (
{/* list available users */} <div className="users-list-container">
</div> {/* list available users here */}
{searchedEmail.map((val: any, i: any) => (
<div onClick={(e) => {
handleAddEmail(val)
setInputFocus(false)
}} key={i} >
{val?.Email}
</div>
))}
</div>
)}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,27 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getProjectSharedList = async (projectId: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/projectsharedList?projectId=${projectId}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (!response.ok) {
throw new Error("Failed to get users");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get users");
console.log(error.message);
}
};

View File

@@ -0,0 +1,27 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getSearchUsers = async (searchMail: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/searchMail?searchMail=${searchMail}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (!response.ok) {
throw new Error("Failed to get users");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get users");
console.log(error.message);
}
};

View File

@@ -0,0 +1,43 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const shareAccess = async (
projectId: string,
targetUserId: string,
newAccessPoint: string
) => {
const body: any = {
projectId,
targetUserId,
newAccessPoint
};
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/sharedAccespoint`,
{
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify(body),
}
);
if (!response.ok) {
console.error("Failed to clearPanel in the zone");
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("An unknown error occurred");
}
}
};

View File

@@ -0,0 +1,30 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const shareProject = async (addUserId: string, projectId: string) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/projectshared`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ addUserId, projectId }),
});
if (!response.ok) {
console.error("Failed to add project");
}
const result = await response.json();
console.log("result: ", result);
return result;
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -46,7 +46,6 @@ textarea {
} }
input[type="number"] { input[type="number"] {
// Chrome, Safari, Edge, Opera // Chrome, Safari, Edge, Opera
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
@@ -487,7 +486,6 @@ input[type="number"] {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
.check-box-style { .check-box-style {
position: absolute; position: absolute;
height: 20px; height: 20px;
@@ -665,6 +663,21 @@ input[type="number"] {
.multi-email-invite-input-container { .multi-email-invite-input-container {
@include flex-space-between; @include flex-space-between;
gap: 20px; gap: 20px;
position: relative;
.users-list-container {
position: absolute;
top: calc(100% + 8px);
background: var(--background-color);
backdrop-filter: blur(18px);
padding: 12px;
width: 100%;
height: auto;
max-height: 200px;
border-radius: 8px;
z-index: 100;
outline: 1px solid var(--border-color);
}
.multi-email-invite-input { .multi-email-invite-input {
width: 100%; width: 100%;
@@ -764,4 +777,4 @@ input[type="number"] {
background: var(--background-color-gray); background: var(--background-color-gray);
} }
} }
} }

View File

@@ -1,13 +1,22 @@
export interface User { export interface User {
name: string; userName: string;
email: string; Email: string;
profileImage: string; Access: string;
color: string; userId: string;
access: string; profileImage?: string;
color?: string;
} }
// export interface User {
// name: string;
// email: string;
// profileImage: string;
// color: string;
// access: string;
// }
type AccessOption = { type AccessOption = {
option: string; option: string;
}; };
export type ActiveUser = { export type ActiveUser = {
@@ -15,7 +24,7 @@ export type ActiveUser = {
userName: string; userName: string;
email: string; email: string;
activeStatus?: string; // Optional property activeStatus?: string; // Optional property
position?: { x: number; y: number; z: number; }; position?: { x: number; y: number; z: number };
rotation?: { x: number; y: number; z: number; }; rotation?: { x: number; y: number; z: number };
target?: { x: number; y: number; z: number; }; target?: { x: number; y: number; z: number };
}; };