Merge remote-tracking branch 'origin/dev-collaboration' into dev-r3f-wall

This commit is contained in:
2025-06-30 18:13:23 +05:30
19 changed files with 794 additions and 185 deletions

View File

@@ -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<void>;
handleTrashDeleteProject?: (projectId: string) => Promise<void>;
handleRestoreProject?: (projectId: string) => Promise<void>;
handleDuplicateWorkspaceProject?: (
projectId: string,
projectName: string,
thumbnail: string
thumbnail: string,
userId?: string
) => Promise<void>;
handleDuplicateRecentProject?: (
projectId: string,
@@ -31,6 +33,7 @@ interface DashBoardCardProps {
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
setRecentDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setProjectDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
}
type RelativeTimeFormatUnit = any;
@@ -45,8 +48,10 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder
}) => {
const navigate = useNavigate();
const { setProjectName } = useProjectName();
@@ -59,10 +64,18 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
const kebabRef = useRef<HTMLDivElement>(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<DashBoardCardProps> = ({
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<DashBoardCardProps> = ({
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<DashBoardCardProps> = ({
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<DashBoardCardProps> = ({
return "just now";
}
const kebabOptionsMap: Record<string, string[]> = {
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 (
<button
className="dashboard-card-container"
@@ -212,13 +248,12 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
<div className="project-data">
{active && active == "trash" ? `Trashed by you` : `Edited `}{" "}
{getRelativeTime(createdAt)}
</div>
)}
</div>
<div className="users-list-container" ref={kebabRef}>
<div className="user-profile">
{userName ? userName.charAt(0).toUpperCase() : "A"}
{(!createdBy) ? userName ? userName?.charAt(0).toUpperCase() : "A" : createdBy?.userName?.charAt(0).toUpperCase()}
</div>
<button
className="kebab-wrapper"
@@ -232,29 +267,9 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
</div>
</div>
</div>
{isKebabOpen && active !== "trash" && (
{isKebabOpen && (
<div className="kebab-options-wrapper">
{["rename", "delete", "duplicate", "open in new tab"].map(
(option) => (
<button
key={option}
className="option"
title={""}
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
)
)}
</div>
)}
{isKebabOpen && active && active == "trash" && (
<div className="kebab-options-wrapper">
{["restore", "delete"].map((option) => (
{getOptions().map((option) => (
<button
key={option}
className="option"

View File

@@ -13,7 +13,7 @@ interface Project {
_id: string;
projectName: string;
thumbnail: string;
createdBy: string;
createdBy: { _id: string, userName: string };
projectUuid?: string;
createdAt: string;
isViewed?: string
@@ -34,7 +34,8 @@ const DashboardHome: React.FC = () => {
const fetchRecentProjects = async () => {
try {
const projects = await recentlyViewed(organization, userId);
console.log('projects: ', projects);
if (JSON.stringify(projects) !== JSON.stringify(recentProjects)) {
setRecentProjects(projects);
}
@@ -126,7 +127,8 @@ const DashboardHome: React.FC = () => {
projectName={project.projectName}
thumbnail={project.thumbnail}
projectId={project._id}
createdAt={project.isViewed}
createdBy={project.createdBy}
createdAt={project.createdAt}
handleDeleteProject={handleDeleteProject}
handleDuplicateRecentProject={handleDuplicateRecentProject}
setRecentDuplicateData={setRecentDuplicateData}

View File

@@ -7,6 +7,8 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { searchProject } from "../../services/dashboard/searchProjects";
import { deleteProject } from "../../services/dashboard/deleteProject";
import ProjectSocketRes from "./socket/projectSocketRes.dev";
import { sharedWithMeProjects } from "../../services/dashboard/sharedWithMeProject";
import { duplicateProject } from "../../services/dashboard/duplicateProject";
interface Project {
_id: string;
@@ -25,12 +27,25 @@ const DashboardProjects: React.FC = () => {
const [workspaceProjects, setWorkspaceProjects] = useState<WorkspaceProjects>(
{}
);
const [sharedwithMeProject, setSharedWithMeProjects] = useState<any>([])
const [projectDuplicateData, setProjectDuplicateData] = useState<Object>({});
const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
const [activeFolder, setActiveFolder] = useState<string>("myProjects");
const { projectSocket } = useSocketStore();
const { userId, organization } = getUserData();
const handleProjectsSearch = async (inputValue: string) => {
if (!inputValue.trim()) {
setIsSearchActive(false);
return;
}
if (!setWorkspaceProjects || !setIsSearchActive) return;
const searchedProject = await searchProject(organization, userId, inputValue);
setIsSearchActive(true);
setWorkspaceProjects(searchedProject.message ? {} : searchedProject);
};
const fetchAllProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
@@ -44,18 +59,6 @@ const DashboardProjects: React.FC = () => {
}
};
const handleProjectsSearch = async (inputValue: string) => {
if (!inputValue.trim()) {
setIsSearchActive(false);
return;
}
if (!setWorkspaceProjects || !setIsSearchActive) return;
const searchedProject = await searchProject(organization, userId, inputValue);
setIsSearchActive(true);
setWorkspaceProjects(searchedProject.message ? {} : searchedProject);
};
const handleDeleteProject = async (projectId: any) => {
try {
// const deletedProject = await deleteProject(
@@ -97,18 +100,9 @@ const DashboardProjects: React.FC = () => {
const handleDuplicateWorkspaceProject = async (
projectId: string,
projectName: string,
thumbnail: string
thumbnail: string,
) => {
// await handleDuplicateProjects({
// userId,
// organization,
// projectId,
// projectName,
// projectSocket,
// thumbnail,
// setWorkspaceProjects,
// setIsSearchActive
// });
const duplicateProjectData = {
userId,
thumbnail,
@@ -121,9 +115,7 @@ const DashboardProjects: React.FC = () => {
const renderProjects = () => {
if (activeFolder !== "myProjects") return null;
const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
if (!projectList?.length) {
return <div className="empty-state">No projects found</div>;
}
@@ -143,12 +135,49 @@ const DashboardProjects: React.FC = () => {
));
};
const renderSharedProjects = () => {
return sharedwithMeProject?.map((project: any) => (
<DashboardCard
key={project._id}
projectName={project.projectName}
thumbnail={project.thumbnail}
projectId={project._id}
createdAt={project.createdAt}
setIsSearchActive={setIsSearchActive}
active="shared"
createdBy={project.createdBy}
setProjectDuplicateData={setProjectDuplicateData}
handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
setActiveFolder={setActiveFolder}
/>
));
};
const sharedProject = async () => {
try {
const sharedWithMe = await sharedWithMeProjects();
console.log('sharedWithMe: ', sharedWithMe);
setSharedWithMeProjects(sharedWithMe)
} catch {
}
}
useEffect(() => {
if (!isSearchActive) {
fetchAllProjects();
}
}, [isSearchActive]);
useEffect(() => {
if (activeFolder === "shared") {
sharedProject()
}
}, [activeFolder])
return (
<div className="dashboard-home-container">
<DashboardNavBar
@@ -171,7 +200,8 @@ const DashboardProjects: React.FC = () => {
Shared with me
</button>
</div>
<div className="cards-container">{renderProjects()}</div>
<div className="cards-container">{activeFolder == "myProjects" ? renderProjects() : renderSharedProjects()}</div>
{projectDuplicateData && Object.keys(projectDuplicateData).length > 0 && (
<ProjectSocketRes
setIsSearchActive={setIsSearchActive}
@@ -183,4 +213,142 @@ const DashboardProjects: React.FC = () => {
);
};
export default DashboardProjects;
export default DashboardProjects;
// const MyProjects = () => {
// const { projectSocket } = useSocketStore();
// const { userId, organization } = getUserData();
// const fetchAllProjects = async () => {
// try {
// const projects = await getAllProjects(userId, organization);
// if (!projects || !projects.Projects) return;
// if (JSON.stringify(projects) !== JSON.stringify(workspaceProjects)) {
// setWorkspaceProjects(projects);
// }
// } catch (error) {
// console.error("Error fetching projects:", error);
// }
// };
// const handleDeleteProject = async (projectId: any) => {
// try {
// // const deletedProject = await deleteProject(
// // projectId,
// // userId,
// // organization
// // );
// // console.log('deletedProject: ', deletedProject);
// const deleteProjects = {
// projectId,
// organization,
// userId,
// };
// //socket for deleting the project
// if (projectSocket) {
// projectSocket.emit("v1:project:delete", deleteProjects);
// } else {
// console.error("Socket is not connected.");
// }
// setWorkspaceProjects((prevDiscardedProjects: WorkspaceProjects) => {
// if (!Array.isArray(prevDiscardedProjects?.Projects)) {
// return prevDiscardedProjects;
// }
// const updatedProjectDatas = prevDiscardedProjects.Projects.filter(
// (project) => project._id !== projectId
// );
// return {
// ...prevDiscardedProjects,
// Projects: updatedProjectDatas,
// };
// });
// setIsSearchActive(false);
// } catch (error) {
// console.error("Error deleting project:", error);
// }
// };
// const handleDuplicateWorkspaceProject = async (
// projectId: string,
// projectName: string,
// thumbnail: string
// ) => {
// // await handleDuplicateProjects({
// // userId,
// // organization,
// // projectId,
// // projectName,
// // projectSocket,
// // thumbnail,
// // setWorkspaceProjects,
// // setIsSearchActive
// // });
// const duplicateProjectData = {
// userId,
// thumbnail,
// organization,
// projectUuid: projectId,
// projectName,
// };
// projectSocket.emit("v1:project:Duplicate", duplicateProjectData);
// };
// const renderProjects = () => {
// if (activeFolder !== "myProjects") return null;
// const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
// if (!projectList?.length) {
// return <div className="empty-state">No projects found</div>;
// }
// return projectList.map((project) => (
// <DashboardCard
// key={project._id}
// projectName={project.projectName}
// thumbnail={project.thumbnail}
// projectId={project._id}
// createdAt={project.createdAt}
// handleDeleteProject={handleDeleteProject}
// setIsSearchActive={setIsSearchActive}
// handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
// setProjectDuplicateData={setProjectDuplicateData}
// />
// ));
// };
// const renderSharedProjects = () => {
// if (activeFolder !== "shared") return null;
// const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
// if (!projectList?.length) {
// return <div className="empty-state">No projects found</div>;
// }
// return projectList.map((project) => (
// <DashboardCard
// key={project._id}
// projectName={project.projectName}
// thumbnail={project.thumbnail}
// projectId={project._id}
// createdAt={project.createdAt}
// handleDeleteProject={handleDeleteProject}
// setIsSearchActive={setIsSearchActive}
// handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
// setProjectDuplicateData={setProjectDuplicateData}
// />
// ));
// };
// useEffect(() => {
// if (!isSearchActive) {
// fetchAllProjects();
// }
// }, [isSearchActive]);
// return null
// }

View File

@@ -14,12 +14,24 @@ interface Project {
createdAt: string;
isViewed?: string
}
interface RecentProject {
_id: string;
projectName: string;
thumbnail: string;
createdBy: { _id: string, userName: string };
projectUuid?: string;
createdAt: string;
isViewed?: string
}
interface RecentProjectData {
[key: string]: RecentProject[];
}
interface ProjectsData {
[key: string]: Project[];
}
interface ProjectSocketResProps {
setRecentProjects?: React.Dispatch<React.SetStateAction<ProjectsData>>;
setRecentProjects?: React.Dispatch<React.SetStateAction<RecentProjectData>>;
setWorkspaceProjects?: React.Dispatch<React.SetStateAction<ProjectsData>>;
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
@@ -52,7 +64,7 @@ const ProjectSocketRes = ({
};
const handleDuplicate = async (data: any) => {
// console.log("Project duplicate response:", data);
console.log("Project duplicate response:", data);
if (data?.message === "Project Duplicated successfully") {
if (setWorkspaceProjects) {
const allProjects = await getAllProjects(userId, organization);

View File

@@ -5,6 +5,8 @@ import { RemoveIcon } from "../../../icons/ExportCommonIcons";
import PositionInput from "../customInput/PositionInputs";
import RotationInput from "../customInput/RotationInput";
import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { center } from "@turf/turf";
interface UserData {
id: number; // Unique identifier for the user data
@@ -18,6 +20,10 @@ const AssetProperties: React.FC = () => {
const { selectedFloorItem } = useSelectedFloorItem();
const { objectPosition } = useObjectPosition();
const { objectRotation } = useObjectRotation();
const { assetStore } = useSceneContext();
const { assets, setCurrentAnimation } = assetStore()
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
const [isPlaying, setIsplaying] = useState(false);
// Function to handle adding new user data
const handleAddUserData = () => {
const newUserData: UserData = {
@@ -45,6 +51,12 @@ const AssetProperties: React.FC = () => {
);
};
const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) {
const isPlaying = selectedFloorItem.animationState?.playing || false;
setCurrentAnimation(selectedFloorItem.uuid, animation, !isPlaying);
}
}
return (
<div className="asset-properties-container">
{/* Name */}
@@ -96,6 +108,40 @@ const AssetProperties: React.FC = () => {
+ Add
</div>
</section>
<div style={{ display: "flex", flexDirection: "column", outline: "1px solid var(--border-color)" }}>
{selectedFloorItem.uuid && <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>Animations</div>}
{assets.map((asset) => (
<div key={asset.modelUuid} className="asset-item">
{asset.modelUuid === selectedFloorItem.uuid &&
asset.animations &&
asset.animations.length > 0 &&
asset.animations.map((animation, index) => (
<div
key={index}
style={{ gap: "15px", cursor: "pointer", padding: "5px" }}
>
<div
onClick={() => handleAnimationClick(animation)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
style={{
height: "20px",
width: "100%",
borderRadius: "5px",
background:
hoveredIndex === index
? "#7b4cd3"
: "transparent",
}}
>
{animation.charAt(0).toUpperCase() +
animation.slice(1).toLowerCase()}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
};

View File

@@ -7,15 +7,23 @@ 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;
}
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)
setAccessSelection(option);
}
@@ -26,18 +34,22 @@ const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => {
{user.profileImage ? (
<img
src={user.profileImage || "https://via.placeholder.com/150"}
alt={`${user.name}'s profile`}
alt={`${user.
userName}'s profile`}
/>
) : (
<div
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 className="user-name">{user.name}</div>
<div className="user-name"> {user.
userName.charAt(0).toUpperCase() + user.
userName.slice(1).toLowerCase()}</div>
</div>
<div className="user-access">
<RegularDropDown
@@ -61,39 +73,27 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
}) => {
const { activeUsers } = useActiveUsers();
const { userName } = getUserData();
const [users, setUsers] = useState([])
const { projectId } = useParams();
function getData() {
if (!projectId) return;
getProjectSharedList(projectId).then((allUser) => {
const accesMail = allUser?.datas || []
console.log('accesMail: ', accesMail);
setUsers(accesMail)
}).catch((err) => {
})
}
useEffect(() => {
// console.log("activeUsers: ", activeUsers);
getData();
}, [])
useEffect(() => {
//
}, [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 (
<RenderOverlay>
<div
@@ -112,7 +112,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
<div className="header">
<div className="content">Share this file</div>
<div className="content">
<div className="copy-link-button">copy link</div>
{/* <div className="copy-link-button">copy link</div> */}
<div
className="close-button"
onClick={() => {
@@ -124,7 +124,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
</div>
</div>
<div className="invite-input-container">
<MultiEmailInvite />
<MultiEmailInvite users={users} getData={getData} />
</div>
<div className="split"></div>
<section>
@@ -142,7 +142,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
<div className="you-container">
<div className="your-name">
<div className="user-profile">{userName && userName[0].toUpperCase()}</div>
{userName}
{userName && userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase()}
</div>
<div className="indicater">you</div>
</div>

View File

@@ -6,6 +6,8 @@ import { useProjectName } from "../../store/builder/store";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { useComparisonProduct } from "../../store/simulation/useSimulationStore";
import { getUserData } from "../../functions/getUserData";
import { recentlyViewed } from "../../services/dashboard/recentlyViewed";
import { sharedWithMeProjects } from "../../services/dashboard/sharedWithMeProject";
interface LoadingPageProps {
progress: number; // Expect progress as a percentage (0-100)
@@ -19,18 +21,51 @@ const LoadingPage: React.FC<LoadingPageProps> = ({ progress }) => {
const validatedProgress = Math.min(100, Math.max(0, progress));
useEffect(() => {
if (!userId) return;
// useEffect(() => {
// if (!userId) return;
getAllProjects(userId, organization).then((projects) => {
if (!projects || !projects.Projects) return;
const filterProject = projects?.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
if (filterProject) {
setProjectName(filterProject.projectName);
// // getAllProjects(userId, organization).then((projects) => {
// // sharedWithMeProjects().then((shared) => {
// // console.log('filterProject: ', shared);
// // const filterProject = (projects?.Projects || shared)?.find((val: any) => val.projectUuid === projectId || val._id === projectId)
// // console.log('filterProject: ', filterProject);
// // // setProjectName(filterProject?.projectName)
// // })
// // }).catch(() => {
// // console.error("Error fetching projects")
// // })
// }, []);
useEffect(() => {
if (!userId) {
console.error("User data not found in localStorage");
return;
}
const fetchProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
const shared = await sharedWithMeProjects();
const allProjects = [...(projects?.Projects || []), ...(shared || [])];
const matchedProject = allProjects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
if (matchedProject) {
setProjectName(matchedProject.projectName);
} else {
console.warn("Project not found with given ID:", projectId);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
}).catch((error) => {
console.log(error);
})
};
fetchProjects();
}, []);
return (

View File

@@ -1,72 +1,135 @@
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<string[]>([]);
interface UserData {
_id: string;
Email: string;
userName: string;
}
interface MultiEmailProps {
users: any,
getData: any,
}
const MultiEmailInvite: React.FC<MultiEmailProps> = ({ users, getData }) => {
const [emails, setEmails] = useState<any>([]);
const [searchedEmail, setSearchedEmail] = useState<UserData[]>([]);
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);
if (isNotCurrentUser && !alreadyExistsInEmails && !alreadyExistsInUsers) {
return [...prev, selectedUser];
}
// Validate email
if (!trimmedEmail || !validateEmail(trimmedEmail)) {
alert("Please enter a valid email address.");
return;
}
// Check for duplicates
if (emails.includes(trimmedEmail)) {
alert("This email has already been added.");
return;
}
// 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<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
handleAddEmail();
if (trimmedEmail.length < 3) return;
try {
const searchedMail = await getSearchUsers(trimmedEmail);
const filteredEmail = searchedMail.sharchMail?.filtered;
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 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([])
setInputValue("")
});
setTimeout(() => {
getData()
}, 1000);
} catch (error) {
console.error("General error:", error);
}
};
return (
<div className="multi-email-invite-input-container">
<div className={`multi-email-invite-input${inputFocus ? " active" : ""}`}>
{emails.map((email, index) => (
{emails.map((email: any, index: number) => (
<div key={index} className="entered-emails">
{email}
<span onClick={() => handleRemoveEmail(email)}>&times;</span>
{email.Email}
<span onClick={() => handleRemoveEmail(email._id)}>&times;</span>
</div>
))}
<input
type="text"
value={inputValue}
onChange={(e) => 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"
/>
</div>
<div onClick={handleAddEmail} className="invite-button">
Invite
</div>
<div className="users-list-container">
{/* list available users */}
<div onClick={handleInvite} className="invite-button">
Add
</div>
{inputFocus && inputValue.length > 2 && searchedEmail && searchedEmail.length > 0 && (
<div className="users-list-container">
{/* list available users here */}
{searchedEmail.map((val: any, i: any) => (
<div onClick={(e) => {
handleAddEmail(val)
setInputFocus(false)
}} key={i} >
{val?.Email}
</div>
))}
</div>
)}
</div>
);
};

View File

@@ -6,7 +6,7 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
import { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store';
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
import { CameraControls } from '@react-three/drei';
import { CameraControls, Html } from '@react-three/drei';
import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore';
import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
@@ -23,7 +23,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { removeAsset } = assetStore();
const { assets, removeAsset, setAnimations } = assetStore();
const { setTop } = useTopData();
const { setLeft } = useLeftData();
const { getIsEventInProduct } = productStore();
@@ -46,6 +46,9 @@ function Model({ asset }: { readonly asset: Asset }) {
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { userId, organization } = getUserData();
const [animationNames, setAnimationNames] = useState<string[]>([]);
const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
useEffect(() => {
setDeletableFloorItem(null);
@@ -60,11 +63,45 @@ function Model({ asset }: { readonly asset: Asset }) {
const loadModel = async () => {
try {
// Check Cache
// const assetId = asset.assetId;
// const cachedModel = THREE.Cache.get(assetId);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
// Check Cache
// const assetId = asset.assetId;
// console.log('assetId: ', assetId);
// const cachedModel = THREE.Cache.get(assetId);
// console.log('cachedModel: ', cachedModel);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
const assetId = asset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
setGltfScene(cachedModel.scene.clone());
calculateBoundingBox(cachedModel.scene);
const clonedScene = cachedModel.scene.clone();
clonedScene.animations = cachedModel.animations || [];
setGltfScene(clonedScene);
calculateBoundingBox(clonedScene);
if (cachedModel.animations && clonedScene.animations.length > 0) {
const animationName = clonedScene.animations.map((clip: any) => clip.name);
setAnimationNames(animationName)
setAnimations(asset.modelUuid, animationName)
mixerRef.current = new THREE.AnimationMixer(clonedScene);
clonedScene.animations.forEach((animation: any) => {
const action = mixerRef.current!.clipAction(animation);
actions.current[animation.name] = action;
});
} else {
console.log('No animations');
}
return;
}
@@ -259,6 +296,32 @@ function Model({ asset }: { readonly asset: Asset }) {
clearSelectedAsset()
}
}
useFrame((_, delta) => {
if (mixerRef.current) {
mixerRef.current.update(delta);
}
});
useEffect(() => {
const handlePlay = (clipName: string) => {
console.log('clipName: ', clipName, asset.animationState);
if (!mixerRef.current) return;
Object.values(actions.current).forEach((action) => action.stop());
const action = actions.current[clipName];
if (action && asset.animationState?.playing) {
action.reset().setLoop(THREE.LoopOnce, 1).play();
console.log(`Playing: ${clipName}`);
} else {
console.warn(`No action found for: ${clipName}`);
}
};
handlePlay(asset.animationState?.current || '');
}, [asset])
return (
<group
@@ -306,7 +369,18 @@ function Model({ asset }: { readonly asset: Asset }) {
<AssetBoundingBox boundingBox={boundingBox} />
)
)}
</group>
{/* <group >
<Html>
<div style={{ position: 'absolute', }}>
{animationNames.map((name) => (
<button key={name} onClick={() => handlePlay(name)} style={{ margin: 4 }}>
{name}
</button>
))}
</div>
</Html>
</group> */}
</group >
);
}

View File

@@ -40,10 +40,10 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
if (!canvas) return;
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
const updateProjects = {
projectId: project.projectUuid,
projectId: project?.projectUuid,
organization,
userId,
projectName: project.projectName,
projectName: project?.projectName,
thumbnail: screenshotDataUrl,
};
if (projectSocket) {

View File

@@ -29,6 +29,8 @@ import { SceneProvider } from "../modules/scene/sceneContext";
import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi";
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
import { VersionProvider } from "../modules/builder/version/versionContext";
import { recentlyViewed } from "../services/dashboard/recentlyViewed";
import { sharedWithMeProjects } from "../services/dashboard/sharedWithMeProject";
const Project: React.FC = () => {
let navigate = useNavigate();
@@ -54,18 +56,32 @@ const Project: React.FC = () => {
return;
}
getAllProjects(userId, organization).then((projects) => {
if (!projects || !projects.Projects) return;
const filterProject = projects?.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId)
setProjectName(filterProject.projectName)
viewProject(organization, filterProject._id, userId).then((viewedProject) => {
});
}).catch(() => {
console.error("Error fetching projects")
})
const fetchProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
const shared = await sharedWithMeProjects();
const allProjects = [...(projects?.Projects || []), ...(shared || [])];
const matchedProject = allProjects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
if (matchedProject) {
setProjectName(matchedProject.projectName);
await viewProject(organization, matchedProject._id, userId);
} else {
console.warn("Project not found with given ID:", projectId);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
fetchProjects();
}, []);
useEffect(() => {
if (!projectId) return;
getVersionHistoryApi(projectId).then((data) => {

View File

@@ -0,0 +1,30 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const sharedWithMeProjects = async (
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/sharedWithMe`, {
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
//console.log("New token received:", newAccessToken);
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
throw new Error("Failed to fetch assets");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get asset image");
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 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

@@ -21,7 +21,7 @@ interface AssetsStore {
setOpacity: (modelUuid: string, opacity: number) => void;
// Animation controls
setAnimation: (modelUuid: string, animation: string) => void;
setAnimations: (modelUuid: string, animations: string[]) => void;
setCurrentAnimation: (modelUuid: string, current: string, isPlaying: boolean) => void;
addAnimation: (modelUuid: string, animation: string) => void;
removeAnimation: (modelUuid: string, animation: string) => void;
@@ -143,14 +143,13 @@ export const createAssetStore = () => {
},
// Animation controls
setAnimation: (modelUuid, animation) => {
setAnimations: (modelUuid, animations) => {
set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid);
if (asset) {
asset.animations = animations;
if (!asset.animationState) {
asset.animationState = { current: animation, playing: false };
} else {
asset.animationState.current = animation;
asset.animationState = { current: '', playing: false };
}
}
});

View File

@@ -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);
}
}
}
}

View File

@@ -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; };
};
position?: { x: number; y: number; z: number };
rotation?: { x: number; y: number; z: number };
target?: { x: number; y: number; z: number };
};