From 13a2648e83fca4ad0eba38d7cc379f3499c095c5 Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Wed, 25 Jun 2025 09:35:47 +0530 Subject: [PATCH 1/5] 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 }; +}; From b3b0831a7f232ffb632a72a3af08de891324f81c Mon Sep 17 00:00:00 2001 From: Poovizhi99 Date: Thu, 26 Jun 2025 09:25:02 +0530 Subject: [PATCH 2/5] Enhance dashboard components with user collaboration features and project management improvements --- .../components/Dashboard/DashboardCard.tsx | 85 ++++--- .../components/Dashboard/DashboardHome.tsx | 8 +- .../Dashboard/DashboardProjects.tsx | 222 +++++++++++++++--- .../Dashboard/socket/projectSocketRes.dev.tsx | 16 +- .../templates/CollaborationPopup.tsx | 22 +- app/src/components/templates/LoadingPage.tsx | 55 ++++- .../components/ui/inputs/MultiEmailInvite.tsx | 18 +- app/src/modules/scene/scene.tsx | 4 +- app/src/pages/Project.tsx | 34 ++- .../services/dashboard/sharedWithMeProject.ts | 30 +++ 10 files changed, 380 insertions(+), 114 deletions(-) create mode 100644 app/src/services/dashboard/sharedWithMeProject.ts diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 47fa261..68d91e7 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, act } from "react"; import img from "../../assets/image/image.png"; import { useNavigate } from "react-router-dom"; import { getUserData } from "../../functions/getUserData"; @@ -14,13 +14,15 @@ interface DashBoardCardProps { projectId: string; createdAt?: string; isViewed?: string; + createdBy?: { _id: string, userName: string }; handleDeleteProject?: (projectId: string) => Promise; handleTrashDeleteProject?: (projectId: string) => Promise; handleRestoreProject?: (projectId: string) => Promise; handleDuplicateWorkspaceProject?: ( projectId: string, projectName: string, - thumbnail: string + thumbnail: string, + userId?: string ) => Promise; handleDuplicateRecentProject?: ( projectId: string, @@ -31,6 +33,7 @@ interface DashBoardCardProps { setIsSearchActive?: React.Dispatch>; setRecentDuplicateData?: React.Dispatch>; setProjectDuplicateData?: React.Dispatch>; + setActiveFolder?: React.Dispatch>; } type RelativeTimeFormatUnit = any; @@ -45,8 +48,10 @@ const DashboardCard: React.FC = ({ handleDuplicateWorkspaceProject, handleDuplicateRecentProject, createdAt, + createdBy, setRecentDuplicateData, setProjectDuplicateData, + setActiveFolder }) => { const navigate = useNavigate(); const { setProjectName } = useProjectName(); @@ -59,10 +64,18 @@ const DashboardCard: React.FC = ({ const kebabRef = useRef(null); const navigateToProject = async (e: any) => { + console.log('active: ', active); if (active && active == "trash") return; - setLoadingProgress(1) - setProjectName(projectName); - navigate(`/${projectId}`); + try { + const viewProjects = await viewProject(organization, projectId, userId) + console.log('viewProjects: ', viewProjects); + console.log('projectName: ', projectName); + setLoadingProgress(1) + setProjectName(projectName); + navigate(`/${projectId}`); + } catch { + + } }; const handleOptionClick = async (option: string) => { @@ -81,11 +94,18 @@ const DashboardCard: React.FC = ({ break; case "open in new tab": try { - await viewProject(organization, projectId, userId); - setProjectName(projectName); - setIsKebabOpen(false); + if (active === "shared" && createdBy) { + console.log("ihreq"); + const newTab = await viewProject(organization, projectId, createdBy?._id); + console.log('newTab: ', newTab); + } else { + const newTab = await viewProject(organization, projectId, userId); + console.log('newTab: ', newTab); + setProjectName(projectName); + setIsKebabOpen(false); + } } catch (error) { - console.error("Error opening project in new tab:", error); + } window.open(`/${projectId}`, "_blank"); break; @@ -100,13 +120,17 @@ const DashboardCard: React.FC = ({ projectName, thumbnail, }); - await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail); + await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId); + if (active === "shared" && setActiveFolder) { + setActiveFolder("myProjects") + } } else if (handleDuplicateRecentProject) { setRecentDuplicateData && setRecentDuplicateData({ projectId, projectName, thumbnail, + userId }); await handleDuplicateRecentProject(projectId, projectName, thumbnail); } @@ -128,7 +152,6 @@ const DashboardCard: React.FC = ({ try { const projects = await getAllProjects(userId, organization); if (!projects || !projects.Projects) return; - // console.log("projects: ", projects); let projectUuid = projects.Projects.find( (val: any) => val.projectUuid === projectId || val._id === projectId ); @@ -173,6 +196,19 @@ const DashboardCard: React.FC = ({ return "just now"; } + const kebabOptionsMap: Record = { + default: ["rename", "delete", "duplicate", "open in new tab"], + trash: ["restore", "delete"], + shared: ["duplicate", "open in new tab"], + }; + + const getOptions = () => { + if (active === "trash") return kebabOptionsMap.trash; + if (active === "shared") return kebabOptionsMap.shared; + if (createdBy && createdBy?._id !== userId) return kebabOptionsMap.shared; + return kebabOptionsMap.default; + }; + return (
- {userName ? userName.charAt(0).toUpperCase() : "A"} + {(!createdBy) ? userName ? userName?.charAt(0).toUpperCase() : "A" : createdBy?.userName?.charAt(0).toUpperCase()}
- - {isKebabOpen && active !== "trash" && ( + {isKebabOpen && (
- {["rename", "delete", "duplicate", "open in new tab"].map( - (option) => ( - - ) - )} -
- )} - {isKebabOpen && active && active == "trash" && ( -
- {["restore", "delete"].map((option) => ( + {getOptions().map((option) => (