Refactor: Enhance CollaborationPopup and MultiEmailInvite components; integrate user access sharing and project user retrieval functionalities; update user data structure for consistency.
This commit is contained in:
@@ -7,15 +7,22 @@ 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 { shareAccess } from "../../services/factoryBuilder/collab/shareAccess";
|
||||||
|
import { getAvatarColor } from "../../modules/collaboration/functions/getAvatarColor";
|
||||||
|
|
||||||
interface UserListTemplateProps {
|
interface UserListTemplateProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
setAccessSelection(option);
|
setAccessSelection(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,18 +33,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 +72,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();
|
||||||
|
|
||||||
|
function getData() {
|
||||||
|
if (!projectId) return;
|
||||||
|
getProjectSharedList(projectId).then((allUser) => {
|
||||||
|
const accesMail = allUser?.datas || []
|
||||||
|
console.log('accesMail: ', accesMail);
|
||||||
|
setUsers(accesMail)
|
||||||
|
}).catch((err) => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log("activeUsers: ", activeUsers);
|
getData();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//
|
||||||
}, [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
|
||||||
@@ -112,7 +111,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
|
|||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="content">Share this file</div>
|
<div className="content">Share this file</div>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="copy-link-button">copy link</div>
|
{/* <div className="copy-link-button">copy link</div> */}
|
||||||
<div
|
<div
|
||||||
className="close-button"
|
className="close-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -124,7 +123,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="invite-input-container">
|
<div className="invite-input-container">
|
||||||
<MultiEmailInvite />
|
<MultiEmailInvite users={users} getData={getData} />
|
||||||
</div>
|
</div>
|
||||||
<div className="split"></div>
|
<div className="split"></div>
|
||||||
<section>
|
<section>
|
||||||
@@ -142,7 +141,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
|
|||||||
<div className="you-container">
|
<div className="you-container">
|
||||||
<div className="your-name">
|
<div className="your-name">
|
||||||
<div className="user-profile">{userName && userName[0].toUpperCase()}</div>
|
<div className="user-profile">{userName && userName[0].toUpperCase()}</div>
|
||||||
{userName}
|
{userName && userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="indicater">you</div>
|
<div className="indicater">you</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,72 +1,135 @@
|
|||||||
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 {
|
||||||
|
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 [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);
|
||||||
|
if (isNotCurrentUser && !alreadyExistsInEmails && !alreadyExistsInUsers) {
|
||||||
|
return [...prev, selectedUser];
|
||||||
|
}
|
||||||
|
|
||||||
// Validate email
|
return prev;
|
||||||
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]);
|
|
||||||
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();
|
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 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([])
|
||||||
|
setInputValue("")
|
||||||
|
});
|
||||||
|
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)}>×</span>
|
<span onClick={() => handleRemoveEmail(email._id)}>×</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
|
Add
|
||||||
</div>
|
|
||||||
<div className="users-list-container">
|
|
||||||
{/* list available users */}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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") || "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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 get users");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error: any) {
|
||||||
|
echo.error("Failed to get users");
|
||||||
|
console.log(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
32
app/src/services/factoryBuilder/collab/getSearchUsers.ts
Normal file
32
app/src/services/factoryBuilder/collab/getSearchUsers.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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") || "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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 get users");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error: any) {
|
||||||
|
echo.error("Failed to get users");
|
||||||
|
console.log(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
48
app/src/services/factoryBuilder/collab/shareAccess.ts
Normal file
48
app/src/services/factoryBuilder/collab/shareAccess.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const newAccessToken = response.headers.get("x-access-token");
|
||||||
|
if (newAccessToken) {
|
||||||
|
//console.log("New token received:", newAccessToken);
|
||||||
|
localStorage.setItem("token", newAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
36
app/src/services/factoryBuilder/collab/shareProject.ts
Normal file
36
app/src/services/factoryBuilder/collab/shareProject.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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 }),
|
||||||
|
});
|
||||||
|
const newAccessToken = response.headers.get("x-access-token");
|
||||||
|
if (newAccessToken) {
|
||||||
|
//console.log("New token received:", newAccessToken);
|
||||||
|
localStorage.setItem("token", newAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
27
app/src/types/users.d.ts
vendored
27
app/src/types/users.d.ts
vendored
@@ -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 };
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user