diff --git a/frontend/components/Admin/Status.js b/frontend/components/Admin/Status.js index cacd42f8..f10aef93 100644 --- a/frontend/components/Admin/Status.js +++ b/frontend/components/Admin/Status.js @@ -13,6 +13,7 @@ export default function Status() { const dispatch = useDispatch(); const token = useSelector(state => state.user.token); const { status, loading } = useStatus(token, dispatch); + if (status) { return (
@@ -91,7 +92,7 @@ export default function Status() { } } -const useStatus = (token, dispatch) => { +export const useStatus = (token, dispatch) => { const { data, error } = useSWR( [`${publicRuntimeConfig.backend}/admin`, token, dispatch], fetcher @@ -117,7 +118,7 @@ const fetcher = (url, token, dispatch) => if (error.response && error.response.status === 401) { // Handle 401 Unauthorized error by signing out and redirecting to the login page dispatch(logoutUser()); // Dispatch the logout action to sign out the user - window.location.href = '/login'; // Redirect to the login page + // window.location.href = '/login'; // Redirect to the login page } else { // Handle other errors error.customMessage = 'Error fetching status'; diff --git a/frontend/components/Viewing/Collection/Members.js b/frontend/components/Viewing/Collection/Members.js index 96136ca8..ca058796 100644 --- a/frontend/components/Viewing/Collection/Members.js +++ b/frontend/components/Viewing/Collection/Members.js @@ -21,6 +21,10 @@ import lookupRole from '../../../namespace/lookupRole'; import Link from 'next/link'; import { addError } from '../../../redux/actions'; import { processUrl } from '../../Admin/Registries'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash, faUnlink } from '@fortawesome/free-solid-svg-icons'; +import { getAfterThirdSlash } from '../ViewHeader'; +import Status, { useStatus } from '../../Admin/Status'; /* eslint sonarjs/cognitive-complexity: "off" */ @@ -77,25 +81,32 @@ export default function Members(properties) { limit: ' LIMIT 10000 ' }; + const { adminStatus, loading, error } = useStatus(token, dispatch); + if (token) { parameters.graphs = privateGraph + } else { } const searchQuery = preparedSearch || typeFilter !== 'Show Only Root Objects'; let query = searchQuery ? getCollectionMembersSearch : getCollectionMembers; - const { members, mutate } = useMembers(query, parameters, token, dispatch); + const { members, mutate } = privateGraph + ? useMembers(query, parameters, token, dispatch) + : useMembers(query, parameters, dispatch); + const { count: totalMemberCount } = useCount( CountMembersTotal, { ...parameters, search: '' }, - token, + privateGraph ? token : undefined, // Pass token only if privateGraph is true dispatch ); + const { count: currentMemberCount } = useCount( searchQuery ? CountMembersTotal : CountMembers, parameters, - token, + privateGraph ? token : undefined, // Pass token only if privateGraph is true dispatch ); @@ -137,6 +148,7 @@ export default function Members(properties) { setSort={setSort} defaultSortOption={defaultSortOption} setDefaultSortOption={setDefaultSortOption} + uri={properties.uri} /> ); @@ -264,7 +276,7 @@ function MemberTable(properties) { customSearch={properties.customSearch} hideFilter={true} searchable={[]} - headers={['Name', 'Identifier', 'Type', 'Description']} + headers={['Name', 'Identifier', 'Type', 'Description', 'Remove']} sortOptions={sortOptions} sortMethods={sortMethods} defaultSortOption={properties.defaultSortOption} @@ -280,6 +292,55 @@ function MemberTable(properties) { textArea.innerHTML = member.displayId; } + const objectUriParts = getAfterThirdSlash(properties.uri); + const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; + + const icon = compareUri(member.uri, `/${objectUriParts}`); + + const handleIconClick = (member) => { + if (icon && icon === faTrash) { + handleDelete(member); + } else if (icon && icon === faUnlink) { + handleUnlink(member); + } + }; + + const handleDelete = (member) => { + if (member.uri && window.confirm("Would you like to remove this item from the collection?")) { + axios.get(`${publicRuntimeConfig.backend}${member.uri}/remove`, { + headers: { + "Accept": "text/plain; charset=UTF-8", + "X-authorization": token + } + }) + .then(response => { + window.location.reload(); + }) + .catch(error => { + console.error('Error removing item:', error); + }); + } + }; + + const handleUnlink = (member) => { + if (member.uri && window.confirm("Would you like to unlink this item from the collection?")) { + axios.post(`${objectUri}/removeMembership`, { + "member": `${publicRuntimeConfig.backend}${member.uri}` + }, { + headers: { + "Accept": "text/plain; charset=UTF-8", + "X-authorization": token + } + }) + .then(response => { + window.location.reload(); + }) + .catch(error => { + console.error('Error removing item:', error); + }); + } + }; + return ( @@ -296,9 +357,13 @@ function MemberTable(properties) { {getType(member)} {member.description} + handleIconClick(member)}> + + ); }} + /> ); } @@ -319,6 +384,26 @@ function getType(member) { return memberType; } +function compareUri(memberUri, baseUri) { + const userUriPrefix = '/user/'; + const publicUriPrefix = '/public/'; + + if (memberUri.startsWith(userUriPrefix)) { + // Check if member.uri matches properties.uri for the first 3 slashes + const matchUri = baseUri.split('/').slice(0, 4).join('/'); + if (memberUri.startsWith(matchUri)) { + return faTrash; + } + } else if (memberUri.startsWith(publicUriPrefix)) { + // Check if member.uri matches properties.uri for the first 2 slashes + const matchUri = baseUri.split('/').slice(0, 3).join('/'); + if (memberUri.startsWith(matchUri)) { + return faTrash; + } + } + return faUnlink; +} + const createUrl = (query, options) => { query = loadTemplate(query, options); return `${publicRuntimeConfig.backend}/sparql?query=${encodeURIComponent( @@ -378,10 +463,10 @@ const fetcher = (url, token, dispatch) => // Check if the user is logged in by looking for 'userToken' in local storage if (!localStorage.getItem('userToken')) { // User is not logged in, redirect to the login page - window.location.href = '/login'; + // window.location.href = '/login'; } } - + // Dispatch the error regardless of whether the user is logged in dispatch(addError(error)); }); diff --git a/frontend/components/Viewing/MetadataInfo.js b/frontend/components/Viewing/MetadataInfo.js index c07d7fc0..3c875d62 100644 --- a/frontend/components/Viewing/MetadataInfo.js +++ b/frontend/components/Viewing/MetadataInfo.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import Link from 'next/link'; import { useSelector } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faPlus, faTrash, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import RenderIcon from './PageJSON/Rendering/RenderIcon'; import styles from '../../styles/view.module.css'; import { useTheme } from '../Admin/Theme'; @@ -14,13 +14,67 @@ import { getAfterThirdSlash } from './ViewHeader'; const { publicRuntimeConfig } = getConfig(); export default function MetadataInfo({ title, link, label, icon, specific, uri }) { - console.log(label); const { theme } = useTheme(); const [isHovered, setIsHovered] = useState(false); const [isEditingSource, setIsEditingSource] = useState(false); const [newSource, setNewSource] = useState(''); const token = useSelector(state => state.user.token); + const [editSourceIndex, setEditSourceIndex] = useState(null); + const [editedSource, setEditedSource] = useState(''); + const [sources, setSources] = useState(processTitle(title)); // Assuming processTitle is your existing function + + + const handleEditSource = (index, source) => { + setEditSourceIndex(index); + setEditedSource(source); + }; + + const handleCancelEdit = () => { + setEditSourceIndex(null); // Reset edit mode + setEditedSource(''); // Clear the edited content + }; + + + const handleSaveEdit = (index, source) => { + // Make an Axios POST request to save the edited source content + if (editedSource.trim() === '') { + // Handle the case where the edited source content is empty (optional) + alert('Source content cannot be empty.'); + return; // Do not proceed with saving an empty source + } + + const objectUriParts = getAfterThirdSlash(uri); + const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; + + axios + .post( + `${objectUri}/edit/wasDerivedFrom`, + { + previous: source, // You may need to pass the source index for identification + object: editedSource, // The edited source content + }, + { + headers: { + 'Accept': 'text/plain; charset=UTF-8', + 'X-authorization': token, + }, + } + ) + .then((response) => { + // Handle the successful response here, if needed + // You may want to update the source in your data or state + const updatedSources = [...sources]; + updatedSources[index] = editedSource; + setSources(updatedSources); + setEditSourceIndex(null); + }) + .catch((error) => { + console.error('Error saving edited source:', error); + // Handle the error, display an error message, etc. + }); + }; + const hoverStyle = { backgroundColor: isHovered ? (theme?.hoverColor || '#00A1E4') : (theme?.themeParameters?.[0]?.value || styles.infoheader.backgroundColor) }; @@ -29,8 +83,6 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } const objectUriParts = getAfterThirdSlash(uri); const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; const editedText = newSource; - console.log(newSource); - console.log(objectUri); axios.post(`${objectUri}/add/wasDerivedFrom`, { object: editedText @@ -41,16 +93,47 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } } }) .then(response => { - // Handle response here - // Refresh the page after successful update - window.location.reload(); + // Assuming response.data contains the updated list of sources + // Update your sources state with the new list + setSources([...sources, processTitle(response.data)]); + setIsEditingSource(false); + setNewSource(''); }) .catch(error => { - // Handle error here console.error('Error adding source:', error); }); }; + + const handleDeleteSource = (event, sourceToDelete) => { + event.stopPropagation(); // Prevent event from propagating to parent elements + + if (window.confirm("Do you want to delete this source?")) { + const objectUriParts = getAfterThirdSlash(uri); + const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; + + // Find the index of the source to delete + const indexToDelete = sources.findIndex(source => source === sourceToDelete); + + axios.post(`${objectUri}/remove/wasDerivedFrom`, { object: sourceToDelete }, { + headers: { + "Accept": "text/plain; charset=UTF-8", + "X-authorization": token + } + }) + .then(response => { + const updatedSources = sources.filter((_, index) => index !== indexToDelete); + setSources(updatedSources); + }) + .catch(error => { + console.error('Error deleting source:', error); + }); + } + }; + + + + // Rendered label with plus icon const renderedLabel = (
@@ -68,33 +151,91 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri }
); - // Check if title is a string and contains commas, then split into an array - const sources = typeof title === 'string' && title.includes(',') ? title.split(',').map(s => s.trim()) : [title]; + // // Check if title is a string and contains commas, then split into an array + // const sources = typeof title === 'string' && title.includes(',') ? title.split(',').map(s => s.trim()) : [title]; + + const urlRegex = /https?:\/\/[^ ,]+/g; // URL regex + + function processTitle(title) { + if (typeof title !== 'string') return [title]; + + // Placeholder for commas inside URLs + const commaPlaceholder = '__COMMA__'; + + // Replace commas inside URLs with the placeholder + const titleWithPlaceholders = title.replace(urlRegex, url => url.replace(/,/g, commaPlaceholder)); + + // Split the title on commas + const parts = titleWithPlaceholders.split(',').map(part => part.trim()); + + // Restore commas in URLs + return parts.map(part => part.replace(new RegExp(commaPlaceholder, 'g'), ',')); + } - // Rendered content for the title with each source as a hyperlink const renderedTitle = ( -
+
- {typeof title === 'string' && title.includes(',') - ? title.split(',').map((source, index) => ( + {sources.map((source, index) => { + let processedSource = source; + if (source.match(urlRegex)) { + processedSource = getAfterThirdSlash(source); + } + return ( + {label === "Source" && source && ( + + )} - )) - : - } + ); + })}
- - {source.trim()} - + {label === "Source" && source && editSourceIndex === index ? ( + // Edit mode +
+ setEditedSource(e.target.value)} + /> + + +
+ ) : ( + // Display mode + + {processedSource} + + )} +
+ {editSourceIndex === index ? null : ( +
+ + +
+ )}
{title}
); - // Rendered section including the header and the title (with or without link) + + + + + + + // Rendered section including the header and the title (without link) const renderedSection = (
{renderedLabel}
+ {renderedTitle} {isEditingSource ? ( -
+
Cancel
- ) : link ? ( - {renderedTitle} - ) : renderedTitle} + ) : null}
- ); return renderedSection; -} +} \ No newline at end of file diff --git a/frontend/components/Viewing/Shell.js b/frontend/components/Viewing/Shell.js index 39c9ae8f..8fdfec9a 100644 --- a/frontend/components/Viewing/Shell.js +++ b/frontend/components/Viewing/Shell.js @@ -16,7 +16,6 @@ import MasterJSON from './PageJSON/MasterJSON'; // import { useDispatch } from 'react-redux'; export default function Shell(properties) { - console.log(properties); const plugins = properties.plugins; const metadata = properties.metadata; diff --git a/frontend/components/Viewing/SidePanel.js b/frontend/components/Viewing/SidePanel.js index e2eb594e..d32004f9 100644 --- a/frontend/components/Viewing/SidePanel.js +++ b/frontend/components/Viewing/SidePanel.js @@ -38,9 +38,6 @@ const { publicRuntimeConfig } = getConfig(); * @param {Any} properties Information from the parent component. */ export default function SidePanel({ metadata, type, json, uri, plugins }) { - - console.log(metadata); - console.log(plugins); const [translation, setTranslation] = useState(0); const [processedUrl, setProcessedUrl] = useState({ original: uri }); const dateCreated = metadata.createdDates.split(", ")[0].replace('T', ' ').replace('Z', ''); diff --git a/frontend/components/Viewing/ViewHeader.js b/frontend/components/Viewing/ViewHeader.js index af67044f..8f7e59bd 100644 --- a/frontend/components/Viewing/ViewHeader.js +++ b/frontend/components/Viewing/ViewHeader.js @@ -16,7 +16,6 @@ import getConfig from "next/config"; const { publicRuntimeConfig } = getConfig(); export default function ViewHeader(properties) { - console.log(properties); var displayTitle = properties.type; if (properties.type.includes('#')) { displayTitle = properties.type.split('#')[1]; diff --git a/frontend/pages/[...view].js b/frontend/pages/[...view].js index 3ced2d50..1bc7e3cb 100644 --- a/frontend/pages/[...view].js +++ b/frontend/pages/[...view].js @@ -32,8 +32,6 @@ export default function View({ data, error }) { ); }, [metadata, token]); - console.log(metadata); - // validate part if (!url || !metadata) return ( diff --git a/frontend/pages/submissions.js b/frontend/pages/submissions.js index a5f85212..1f99e2fe 100644 --- a/frontend/pages/submissions.js +++ b/frontend/pages/submissions.js @@ -130,11 +130,7 @@ const options = [ ]; const compareStrings = (string1, string2) => { - if (string1 && string2) { - return (string1.toLowerCase() > string2.toLowerCase() && 1) || -1; - } - return -1; - + return (string1.toLowerCase() > string2.toLowerCase() && 1) || -1; }; const sortMethods = { @@ -222,7 +218,7 @@ const fetcher = (url, token, dispatch) => .catch(error => { if (error.response && error.response.status === 401) { dispatch(logoutUser()); // Dispatch the logout action to sign out the user - window.location.href = '/login'; // Redirect to the login page + // window.location.href = '/login'; // Redirect to the login page } else { // Handle other errors error.customMessage = 'Request(s) failed for submissions data. Check the URL to see which one failed';