diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 8475050e..7b82fa7e 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -290,7 +290,7 @@ export const getVariants = async (group: string, term: string) => { }; export const getVersions = async (group: string, term: string) => { - return createGetRequest(`/${group}/versions/${term}`, "application/json")(); + return createGetRequest(`/${group}/${term}/versions`, "application/json")(); }; export const getTermDiscussions = async (group: string, variantID: string) => { diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index 07dc73f8..0dad806a 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -46,13 +46,19 @@ const Login = () => { let eventer = window[eventMethod]; let messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; eventer(messageEvent, async function (e) { - if (!e.data || !e.data.orcid_meta) return; - const { code, cookies, groupname } = e.data; - if (code === 200 || code === 302) { + if (e.data?.source === "react-devtools-bridge") return; // Ignore messages from React DevTools + if (!(e.data?.orcid_meta || e.data?.redirect || e.data?.interlex)) return; + const { cookies } = e.data; + const { code, errors, redirect, groupname } = e.data.interlex; + + if (cookies) { const _cookies = JSON.parse(cookies); const sessionCookie = _cookies && Object.prototype.hasOwnProperty.call(_cookies, 'session') ? _cookies['session'] : undefined; let expires = new Date() - if (sessionCookie && (existingCookies['session'] === undefined)) { + if (sessionCookie) { + if (existingCookies['session']) { + removeCookie('session', { path: '/' }); + } expires.setTime(expires.getTime() + (2 * 24 * 60 * 60 * 1000)); // 2 days setCookie( 'session', @@ -65,24 +71,24 @@ const Login = () => { } ); } - // Check if the session cookie is present - if (!sessionCookie) { - setErrors((prev) => ({ - ...prev, - auth: "Session cookie not found. Please try again", - })); - return; - } + localStorage.setItem(API_CONFIG.SESSION_DATA.COOKIE, JSON.stringify({ + name: 'session', + value: sessionCookie, + expires: expires + })); + localStorage.setItem("token", sessionCookie) + } + + if (redirect) { + handleRedirectInPopup(redirect); + return; + } + + if (code === 200 || code === 302) { // Retrieve user settings try { const userData = await requestUserSettings(groupname); localStorage.setItem(API_CONFIG.SESSION_DATA.SETTINGS, JSON.stringify(userData)); - localStorage.setItem(API_CONFIG.SESSION_DATA.COOKIE, JSON.stringify({ - name: 'session', - value: sessionCookie, - expires: expires - })); - localStorage.setItem("token", sessionCookie) setUserData({ name: userData['groupname'], id: userData['orcid'], @@ -100,11 +106,16 @@ const Login = () => { auth: "Failed to fetch user settings. Please try again", })); } - } else if (code === 401) { - setErrors((prev) => ({ - ...prev, - auth: "Invalid username or password. Please try again", - })); + } else if (code > 400 && code < 500) { + let errorMessage = ''; + const keys = Object.keys(errors); + if (keys.length > 0) { + errorMessage = String(keys[0]) + ' ' + errors[keys[0]]; + } + setErrors((prev) => ({ + ...prev, + auth: errorMessage || "Invalid username or password. Please try again", + })); } else { setErrors((prev) => ({ ...prev, @@ -116,6 +127,22 @@ const Login = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoading]); + const handleRedirectInPopup = (url) => { + setErrors({}) + setIsLoading(true); + const popup = window.open(url.includes('?') ? url + "&aspopup=true" : url + "?aspopup=true", "postPopup", "width=600,height=600"); + if (popup) { + // dataForm.submit(); + popup.focus(); + } else { + alert("Popup blocked. Please allow popups for this site."); + setErrors((prev) => ({ + ...prev, + auth: "Popup blocked. Please allow popups for this site.", + })); + } + } + const handleInputChange = (e) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); @@ -129,10 +156,30 @@ const Login = () => { const result = await login({ username: formData.username, password: formData.password }) if (!result.data || !result.data?.orcid_meta) { - setErrors((prev) => ({ - ...prev, - auth: "Interlex API is not returning the user information, reminder to ask Tom to send the groupname back so that we can query the priv/setting endpoint to get the rest of the info required", - })); + // setErrors((prev) => ({ + // ...prev, + // auth: "Interlex API is not returning the user information, reminder to ask Tom to send the groupname back so that we can query the priv/setting endpoint to get the rest of the info required", + // })); + try { + const userData = await requestUserSettings(formData.username); + localStorage.setItem(API_CONFIG.SESSION_DATA.SETTINGS, JSON.stringify(userData)); + setUserData({ + name: userData['groupname'], + id: userData['orcid'], + email: userData?.emails[0]?.email, + role: userData['own-role'], + groupname: userData['groupname'], + settings: userData + }); + navigate("/") + } catch (error) { + console.error("Error fetching user settings:", error); + removeCookie('session', { path: '/' }); + setErrors((prev) => ({ + ...prev, + auth: "Failed to fetch user settings. Please try again", + })); + } } else { const { code, orcid_meta } = result.data; if (code === 200 || code === 302) { diff --git a/src/components/Auth/Register.jsx b/src/components/Auth/Register.jsx index 5a07da39..c7293b5b 100644 --- a/src/components/Auth/Register.jsx +++ b/src/components/Auth/Register.jsx @@ -12,6 +12,7 @@ import { import * as yup from "yup"; import FormField from "./UI/Formfield"; import { API_CONFIG } from "../../config"; +import { useCookies } from 'react-cookie'; import PasswordField from "./UI/PasswordField"; import { ArrowBack } from "@mui/icons-material"; import { Link, useNavigate } from "react-router-dom"; @@ -22,6 +23,9 @@ const schema = yup.object().shape({ email: yup.string().email().required(), username: yup.string().required().min(3), password: yup.string().required().min(10), + confirmPassword: yup.string() + .required('Please confirm your password') + .oneOf([yup.ref('password'), null], 'Passwords must match'), }); const Register = () => { @@ -29,11 +33,13 @@ const Register = () => { username: "", email: "", password: "", + confirmPassword: "", }); const [errors, setErrors] = React.useState({}); const [isLoading, setIsLoading] = React.useState(false); const { setUserData } = React.useContext(GlobalDataContext); + const [existingCookies, setCookie, removeCookie] = useCookies(['session']); const navigate = useNavigate(); React.useEffect(() => { @@ -41,16 +47,49 @@ const Register = () => { let eventer = window[eventMethod]; let messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; eventer(messageEvent, function (e) { - if (!e.data || !e.data.orcid_meta) return; - const { code, orcid_meta } = e.data; + if (!(e.data?.orcid_meta || e.data?.redirect || e.data?.interlex)) return; + const { cookies } = e.data; + const { code, orcid_meta, errors, redirect } = e.data.interlex; + + if (cookies) { + const _cookies = JSON.parse(cookies); + const sessionCookie = _cookies && Object.prototype.hasOwnProperty.call(_cookies, 'session') ? _cookies['session'] : undefined; + let expires = new Date() + if (sessionCookie) { + if (existingCookies['session']) { + removeCookie('session', { path: '/' }); + } + expires.setTime(expires.getTime() + (2 * 24 * 60 * 60 * 1000)); // 2 days + setCookie( + 'session', + sessionCookie, + { + path: '/', + secure: false, + sameSite: false, + httpOnly: false + } + ); + } + } + + if (redirect) { + handleRedirectInPopup(redirect); + return; + } if (code === 200 || code === 302) { setUserData({ name: orcid_meta.name, id: orcid_meta.orcid }); navigate("/") - } else if (code === 401) { + } else if (code > 400 && code < 500) { + let errorMessage = ''; + const keys = Object.keys(errors); + if (keys.length > 0) { + errorMessage = String(keys[0]) + ' ' + errors[keys[0]]; + } setErrors((prev) => ({ ...prev, - auth: "Invalid username or password. Please try again", + auth: errorMessage || "Invalid username or password. Please try again", })); } else { setErrors((prev) => ({ @@ -64,6 +103,22 @@ const Register = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoading]); + const handleRedirectInPopup = (url) => { + setErrors({}) + setIsLoading(true); + const popup = window.open(url.includes('?') ? url + "&aspopup=true" : url + "?aspopup=true", "postPopup", "width=600,height=600"); + if (popup) { + // dataForm.submit(); + popup.focus(); + } else { + alert("Popup blocked. Please allow popups for this site."); + setErrors((prev) => ({ + ...prev, + auth: "Popup blocked. Please allow popups for this site.", + })); + } + } + const registerUser = async () => { try { await schema.validate(formData, { abortEarly: false }) @@ -72,7 +127,7 @@ const Register = () => { // send a POST request to the server with the form data in a popup window const dataForm = document.createElement("form"); - dataForm.action = `${API_CONFIG.REAL_API.NEWUSER_ILX}`; + dataForm.action = `${API_CONFIG.REAL_API.NEWUSER_ILX}?from=orcid-login&aspopup=true`; dataForm.method = "POST"; dataForm.style.display = "none"; dataForm.target = "postPopup"; @@ -161,9 +216,24 @@ const Register = () => { errorMessage={errors.password} helperText="Required" /> + + setFormData({ ...formData, confirmPassword: e.target.value }) + } + errorMessage={errors.confirmPassword} + helperText="Passwords must match" + /> - diff --git a/src/components/SingleTermView/History/HistoryItem.jsx b/src/components/SingleTermView/History/HistoryItem.jsx index 8e4d0c89..e60e560b 100644 --- a/src/components/SingleTermView/History/HistoryItem.jsx +++ b/src/components/SingleTermView/History/HistoryItem.jsx @@ -5,53 +5,39 @@ import { ListItemText, ListItemIcon, Stack, - Avatar, Typography } from "@mui/material"; -import { - CreateForkHistoryIcon, - MergeForkHistoryIcon, - MergeHistoryIcon, -} from "../../../Icons"; -import CustomButton from "../../common/CustomButton"; -import RestoreIcon from '@mui/icons-material/Restore'; -import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; +import { CreateForkHistoryIcon } from "../../../Icons"; +// import CustomButton from "../../common/CustomButton"; +// import RestoreIcon from '@mui/icons-material/Restore'; import { vars } from "../../../theme/variables"; const { gray200, gray600, gray700, brand600 } = vars; -const getText = (entry) => { - switch (entry.action) { - case "create": - return `${entry.author} created a fork:`; - case "merge": - return `${entry.author} merged a fork:`; - case "suggest": - return `${entry.author} suggested some changes`; - case "request": - return `${entry.author} requested to merge`; - default: - return `${entry.author} performed an action`; - } -}; +const formatDate = (dateString) => { + const fixedDateString = dateString.replace(',', '.'); + try { + const date = new Date(fixedDateString); + const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const weekday = weekdays[date.getUTCDay()]; + + let hours = date.getUTCHours(); + const ampm = hours >= 12 ? 'pm' : 'am'; + hours = hours % 12; + hours = hours ? hours : 12; + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); -const getIcon = (action) => { - switch (action) { - case "create": - return ; - case "merge": - return ; - case "request": - return ; - default: - return
; + return `${weekday} ${hours}:${minutes}${ampm}`; + } catch (e) { + console.error("Error formatting date:", dateString, e); + return dateString.split('T')[0]; } }; -const visibilityHidden = { - display: 'none', - transition: 'opacity 0.3s ease-in-out' -} +// const visibilityHidden = { +// display: 'none', +// transition: 'opacity 0.3s ease-in-out' +// } const HistoryItem = ({ entry }) => ( ( }}> - {getIcon(entry.action)} + - {entry.author.slice(0, 2)} ( }} primary={ - {getText(entry)} - {entry?.fork} + + A fork of this instance has been created: + + + {entry.fork} + } secondary={ - {entry.date} - {entry.action === 'request' ? ( - - Go to - - ) : ( - - - Restore version - )} + + {formatDate(entry.date)} + + {/* + + Restore version + */} } /> @@ -112,4 +98,4 @@ HistoryItem.propTypes = { entry: PropTypes.object.isRequired }; -export default HistoryItem; +export default HistoryItem; \ No newline at end of file diff --git a/src/components/SingleTermView/History/HistoryPanel.jsx b/src/components/SingleTermView/History/HistoryPanel.jsx index fcf9f937..b2fe31d0 100644 --- a/src/components/SingleTermView/History/HistoryPanel.jsx +++ b/src/components/SingleTermView/History/HistoryPanel.jsx @@ -1,48 +1,74 @@ import React from "react"; +import PropTypes from 'prop-types'; import HistoryItem from "./HistoryItem"; -import { Box, List } from "@mui/material"; +import { Box, List, CircularProgress } from "@mui/material"; import { getVersions } from "../../../api/endpoints/apiService"; - import { vars } from "../../../theme/variables"; -const { gray50 } = vars; -const historyEntries = [ - { author: "Phoenix Baker", action: "merge", date: "Friday 2:05pm", fork: "ForkPB-2" }, - { author: "Phoenix Baker", action: "create", date: "Friday 2:05pm", fork: "ForkPB-2" }, - { author: "Phoenix Baker", action: "suggest", date: "Friday 2:05pm" }, - { author: "Phoenix Baker", action: "request", date: "Friday 2:05pm" }, -]; +const { gray50 } = vars; -const HistoryPanel = () => { - // eslint-disable-next-line no-unused-vars +const HistoryPanel = ({ searchTerm, group = "base" }) => { const [versions, setVersions] = React.useState([]); + const [loading, setLoading] = React.useState(true); React.useEffect(() => { - getVersions("base", "ILX_....").then( data => { - setVersions(data); - }) - }, []); - - return - - {historyEntries.map((entry, index) => ( - - - - ))} - + getVersions(group, searchTerm).then(data => { + const oldestEntries = data.versions.map(version => { + const oldestAppearance = [...version.appears_in].sort((a, b) => + new Date(a.first_seen.replace(',', '.')) - new Date(b.first_seen.replace(',', '.')) + )[0]; + + const uriParts = oldestAppearance.uri.split('http://uri.interlex.org/')[1].split('/'); + const forkName = uriParts[0]; + + return { + date: oldestAppearance.first_seen, + fork: forkName, + identityGraph: version["identity-graph"] + }; + }).sort((a, b) => + new Date(a.date.replace(',', '.')) - new Date(b.date.replace(',', '.')) + ); + setVersions(oldestEntries); + setLoading(false); + }); + }, [group, searchTerm]); + + if (loading) { + return + + + } + + if (!versions.length) return + No version history found + + return ( + + + {versions.map((entry, index) => ( + + + + ))} + + + ); }; +HistoryPanel.propTypes = { + searchTerm: PropTypes.string, + group: PropTypes.string +} + export default HistoryPanel; diff --git a/src/components/SingleTermView/Variants/VariantsPanel.jsx b/src/components/SingleTermView/Variants/VariantsPanel.jsx index 3638a8da..d1e3dcc6 100644 --- a/src/components/SingleTermView/Variants/VariantsPanel.jsx +++ b/src/components/SingleTermView/Variants/VariantsPanel.jsx @@ -1,6 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { Box } from '@mui/material'; +import { Box, CircularProgress } from '@mui/material'; import VariantsTable from './VariantsTable'; import { getVariants } from '../../../api/endpoints/apiService'; @@ -9,19 +9,30 @@ const headCells = [ { id: 'description', label: 'Description' }, { id: 'timestamp', label: 'Timestamp' }, { id: 'status', label: 'Status' }, - { id: 'originated_user', label: 'Originated User' }, + { id: 'originated_user', label: 'Originating User' }, { id: 'editing_user', label: 'Editing User' }, { id: 'action_buttons', label: '' } ]; -const VariantsPanel = () => { +const VariantsPanel = ({ searchTerm, group = "base" }) => { const [variants, setVariants] = React.useState([]); - + const [loading, setLoading] = React.useState(true); + React.useEffect(() => { - getVariants("base","ILX_").then( data => { + getVariants(group, searchTerm).then(data => { setVariants(data); + setLoading(false); }) - }, []); + }, [group, searchTerm]); + + if (loading) { + return + + + } + if (!variants.length) return + No variants found + return ( @@ -31,7 +42,8 @@ const VariantsPanel = () => { } VariantsPanel.propTypes = { - variants: PropTypes.array + searchTerm: PropTypes.string, + group: PropTypes.string } export default VariantsPanel; diff --git a/src/components/SingleTermView/Variants/VariantsTable.jsx b/src/components/SingleTermView/Variants/VariantsTable.jsx index 437859c3..f182579d 100644 --- a/src/components/SingleTermView/Variants/VariantsTable.jsx +++ b/src/components/SingleTermView/Variants/VariantsTable.jsx @@ -71,8 +71,7 @@ const VariantsTable = ({ rows, headCells }) => { const sortedRows = React.useMemo( () => stableSort(rows, getComparator(order, orderBy)), - // eslint-disable-next-line react-hooks/exhaustive-deps - [order, orderBy] + [order, orderBy, rows] ); const displayedRows = React.useMemo( @@ -107,12 +106,12 @@ const VariantsTable = ({ rows, headCells }) => { /> - {row.originated_user} - {row.originated_user_email} + {row.originatingUser.userName} + {/* {row.originated_user_email} */} - {row.editing_user} - {row.editing_user_email} + {row.editingUser.userName} + {/* {row.editing_user_email} */} diff --git a/src/components/SingleTermView/index.jsx b/src/components/SingleTermView/index.jsx index 7191e7e1..4d6bebac 100644 --- a/src/components/SingleTermView/index.jsx +++ b/src/components/SingleTermView/index.jsx @@ -235,13 +235,13 @@ const SingleTermView = () => { case 1: return ; case 2: - return ; + return ; case 3: return ; default: return ; } - }, [tabValue, searchTerm, isCodeViewVisible, selectedDataFormat, actualGroup]); + }, [tabValue, searchTerm, isCodeViewVisible, selectedDataFormat, actualGroup, group]); // Memoize the toggle button group for overview tab const toggleButtonGroup = useMemo(() => { @@ -282,7 +282,7 @@ const SingleTermView = () => { return ( <> - +