From 13a2648e83fca4ad0eba38d7cc379f3499c095c5 Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Wed, 25 Jun 2025 09:35:47 +0530 Subject: [PATCH] Implement collaboration features with user access management and email invitation functionality --- .../templates/CollaborationPopup.tsx | 82 ++++++----- .../components/ui/inputs/MultiEmailInvite.tsx | 133 ++++++++++++++---- .../collab/getProjectSharedList.ts | 27 ++++ .../factoryBuilder/collab/getSearchUsers.ts | 27 ++++ .../factoryBuilder/collab/shareAccess.ts | 43 ++++++ .../factoryBuilder/collab/shareProject.ts | 30 ++++ app/src/styles/components/input.scss | 19 ++- app/src/types/users.d.ts | 29 ++-- 8 files changed, 307 insertions(+), 83 deletions(-) create mode 100644 app/src/services/factoryBuilder/collab/getProjectSharedList.ts create mode 100644 app/src/services/factoryBuilder/collab/getSearchUsers.ts create mode 100644 app/src/services/factoryBuilder/collab/shareAccess.ts create mode 100644 app/src/services/factoryBuilder/collab/shareProject.ts diff --git a/app/src/components/templates/CollaborationPopup.tsx b/app/src/components/templates/CollaborationPopup.tsx index c3c81ae..6093955 100644 --- a/app/src/components/templates/CollaborationPopup.tsx +++ b/app/src/components/templates/CollaborationPopup.tsx @@ -7,15 +7,27 @@ import { access } from "fs"; import MultiEmailInvite from "../ui/inputs/MultiEmailInvite"; import { useActiveUsers } from "../../store/builder/store"; 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 { user: User; } - +interface UserData { + _id: string; + Email: string; + userName: string +} const UserListTemplate: React.FC = ({ user }) => { - const [accessSelection, setAccessSelection] = useState(user.access); - - function accessUpdate({ option }: AccessOption) { + const [accessSelection, setAccessSelection] = useState(user?.Access); + const { projectId } = useParams() + const accessUpdate = async ({ option }: AccessOption) => { + if (!projectId) return + const accessSelection = await shareAccess(projectId, user.userId, option) + console.log('accessSelection: ', accessSelection); setAccessSelection(option); } @@ -26,18 +38,22 @@ const UserListTemplate: React.FC = ({ user }) => { {user.profileImage ? ( {`${user.name}'s ) : (
- {user.name[0]} + {user. + userName.charAt(0).toUpperCase()}
)} -
{user.name}
+
{user. + userName.charAt(0).toUpperCase() + user. + userName.slice(1).toLowerCase()}
= ({ }) => { const { activeUsers } = useActiveUsers(); const { userName } = getUserData(); + const [users, setUsers] = useState([]) + const { projectId } = useParams(); + const [searchedEmail, setSearchedEmail] = useState([]); + const [emails, setEmails] = useState([]); + + function getData() { + if (!projectId) return; + getProjectSharedList(projectId).then((allUser) => { + const accesMail = allUser?.datas || [] + setUsers(accesMail) + }).catch((err) => { + console.log(err); + }) + } + + useEffect(() => { + getData(); + }, []) useEffect(() => { // console.log("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 (
= ({
- +
diff --git a/app/src/components/ui/inputs/MultiEmailInvite.tsx b/app/src/components/ui/inputs/MultiEmailInvite.tsx index a16de66..3cd4b92 100644 --- a/app/src/components/ui/inputs/MultiEmailInvite.tsx +++ b/app/src/components/ui/inputs/MultiEmailInvite.tsx @@ -1,72 +1,143 @@ 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 = () => { - const [emails, setEmails] = useState([]); +interface UserData { + _id: string; + Email: string; + userName: string; + +} + +interface MultiEmailProps { + searchedEmail: UserData[]; + setSearchedEmail: React.Dispatch> + users: any, + getData: any, +} +const MultiEmailInvite: React.FC = ({ searchedEmail, setSearchedEmail, users, getData }) => { + console.log('users: ', users); + const [emails, setEmails] = useState([]); + const [inputFocus, setInputFocus] = useState(false); 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(); + 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 - if (!trimmedEmail || !validateEmail(trimmedEmail)) { - alert("Please enter a valid email address."); - return; - } + console.log('alreadyExistsInEmails: ', alreadyExistsInEmails); + console.log('alreadyExistsInUsers:', alreadyExistsInUsers); - // Check for duplicates - if (emails.includes(trimmedEmail)) { - alert("This email has already been added."); - return; - } + if (isNotCurrentUser && !alreadyExistsInEmails && !alreadyExistsInUsers) { + return [...prev, selectedUser]; + } - // Add email to the list - setEmails([...emails, trimmedEmail]); + return prev; + }); 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) => { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - handleAddEmail(); + if (trimmedEmail.length < 3) return; + try { + const searchedMail = await getSearchUsers(trimmedEmail); + 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) => { + 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 emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 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 (
- {emails.map((email, index) => ( + {emails.map((email: any, index: number) => (
- {email} - handleRemoveEmail(email)}>× + {email.Email} + handleRemoveEmail(email._id)}>×
))} setInputValue(e.target.value)} + onChange={(e) => handleSearchMail(e)} onFocus={() => setInputFocus(true)} - onBlur={() => setInputFocus(false)} + // onBlur={() => setInputFocus(false)} onKeyDown={handleKeyDown} placeholder="Enter email and press Enter or comma to seperate" />
-
+
Invite
-
- {/* list available users */} -
+ {inputFocus && inputValue.length > 0 && searchedEmail && searchedEmail.length > 0 && ( +
+ {/* list available users here */} + {searchedEmail.map((val: any, i: any) => ( +
{ + handleAddEmail(val) + setInputFocus(false) + }} key={i} > + {val?.Email} +
+ ))} +
+ )}
); }; diff --git a/app/src/services/factoryBuilder/collab/getProjectSharedList.ts b/app/src/services/factoryBuilder/collab/getProjectSharedList.ts new file mode 100644 index 0000000..5260a7a --- /dev/null +++ b/app/src/services/factoryBuilder/collab/getProjectSharedList.ts @@ -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 ", + "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); + } +}; diff --git a/app/src/services/factoryBuilder/collab/getSearchUsers.ts b/app/src/services/factoryBuilder/collab/getSearchUsers.ts new file mode 100644 index 0000000..5915a11 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/getSearchUsers.ts @@ -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 ", + "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); + } +}; diff --git a/app/src/services/factoryBuilder/collab/shareAccess.ts b/app/src/services/factoryBuilder/collab/shareAccess.ts new file mode 100644 index 0000000..4ecbf96 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/shareAccess.ts @@ -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 ", + "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"); + } + } +}; diff --git a/app/src/services/factoryBuilder/collab/shareProject.ts b/app/src/services/factoryBuilder/collab/shareProject.ts new file mode 100644 index 0000000..8ce2ff5 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/shareProject.ts @@ -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 ", + "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"); + } + } +}; diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/input.scss index 829a327..a370d56 100644 --- a/app/src/styles/components/input.scss +++ b/app/src/styles/components/input.scss @@ -46,7 +46,6 @@ textarea { } input[type="number"] { - // Chrome, Safari, Edge, Opera &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { @@ -487,7 +486,6 @@ input[type="number"] { position: relative; cursor: pointer; - .check-box-style { position: absolute; height: 20px; @@ -665,6 +663,21 @@ input[type="number"] { .multi-email-invite-input-container { @include flex-space-between; 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 { width: 100%; @@ -764,4 +777,4 @@ input[type="number"] { background: var(--background-color-gray); } } -} \ No newline at end of file +} diff --git a/app/src/types/users.d.ts b/app/src/types/users.d.ts index 02bc3c5..2267570 100644 --- a/app/src/types/users.d.ts +++ b/app/src/types/users.d.ts @@ -1,13 +1,22 @@ export interface User { - name: string; - email: string; - profileImage: string; - color: string; - access: string; + userName: string; + Email: string; + Access: string; + userId: string; + profileImage?: string; + color?: string; + } +// export interface User { +// name: string; +// email: string; +// profileImage: string; +// color: string; +// access: string; +// } type AccessOption = { - option: string; + option: string; }; export type ActiveUser = { @@ -15,7 +24,7 @@ export type ActiveUser = { userName: string; email: string; activeStatus?: string; // Optional property - position?: { x: number; y: number; z: number; }; - rotation?: { x: number; y: number; z: number; }; - target?: { x: number; y: number; z: number; }; -}; \ No newline at end of file + position?: { x: number; y: number; z: number }; + rotation?: { x: number; y: number; z: number }; + target?: { x: number; y: number; z: number }; +};