From f900e6fd2ef9c4f0ce03634d765e2216418e249b Mon Sep 17 00:00:00 2001 From: Brody Wingrove <88740722+BroD54@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:40:13 -0600 Subject: [PATCH 1/6] Collections members section matches search page table style --- .../components/Viewing/Collection/Members.js | 571 +++++++++++------- frontend/styles/view.module.css | 111 +++- 2 files changed, 450 insertions(+), 232 deletions(-) diff --git a/frontend/components/Viewing/Collection/Members.js b/frontend/components/Viewing/Collection/Members.js index 567593c3..0cb05495 100644 --- a/frontend/components/Viewing/Collection/Members.js +++ b/frontend/components/Viewing/Collection/Members.js @@ -13,11 +13,13 @@ import getCollectionMembers from '../../../sparql/getCollectionMembers'; import getCollectionMembersSearch from '../../../sparql/getCollectionMembersSearch'; import getTypesRoles from '../../../sparql/getTypesRoles'; import styles from '../../../styles/view.module.css'; +import tableStyles from '../../../styles/resulttable.module.css'; import MiniLoading from '../../Reusable/MiniLoading'; -import Table from '../../Reusable/Table/Table'; +import { numberDisplayOptions } from '../../Reusable/Table/TableConfig'; import loadTemplate from '../../../sparql/tools/loadTemplate'; import { shortName } from '../../../namespace/namespace'; import lookupRole from '../../../namespace/lookupRole'; +import { getTypeColor } from '../../../utilities/typeColor'; import Link from 'next/link'; import { addError, logoutUser } from '../../../redux/actions'; import { processUrl } from '../../Admin/Registries'; @@ -48,6 +50,8 @@ export default function Members(properties) { const [defaultSortOption, setDefaultSortOption] = useState(sortOptions[0]); const [customBounds, setCustomBounds] = useState([0, 10000]); const [typeFilter, setTypeFilter] = useState('Show Only Root Objects'); + const [numberEntries, setNumberEntries] = useState(numberDisplayOptions[0].value); + const [displayOffset, setDisplayOffset] = useState(0); const dispatch = useDispatch(); const [processedUri, setProcessedUri] = useState(publicRuntimeConfig.backend); const theme = JSON.parse(localStorage.getItem('theme')) || {}; @@ -82,7 +86,7 @@ export default function Members(properties) { const parameters = { from: '', - graphPrefix: `${theme.uriPrefix}`, // TODO: Maybe get this from somewhere? + graphPrefix: `${theme.uriPrefix}`, // TODO: Maybe get this from somewhere? collection: properties.uri, sort: sort, search: preparedSearch, @@ -163,6 +167,16 @@ export default function Members(properties) { setOffset(newBounds[0]); }; + useEffect(() => { + if (displayOffset < customBounds[0] || displayOffset > customBounds[1]) { + outOfBoundsHandle(displayOffset); + } + }, [displayOffset, customBounds]); + + useEffect(() => { + setDisplayOffset(0); + }, [search, sort, typeFilter]); + useEffect(() => { let isMounted = true; async function processUri() { @@ -173,70 +187,82 @@ export default function Members(properties) { processUri(); return () => { isMounted = false }; }, [dispatch]); + + const [processedMembers, setProcessedMembers] = useState([]); + + useEffect(() => { + async function processMembers() { + if (members) { + const updatedMembers = await Promise.all(members.map(async member => { + const processed = await processUrl(member.uri, registries); + return { + ...member, + uri: processed.urlRemovedForLink + }; + })); + setProcessedMembers(updatedMembers); + } + } + + processMembers(); + }, [members]); + + const additionalOffset = customBounds[0]; + const pageStart = Math.max(0, displayOffset - additionalOffset); + const pageEnd = + numberEntries === 'all' + ? processedMembers.length + : Math.min(processedMembers.length, pageStart + numberEntries); + const pageMembers = processedMembers.slice(pageStart, pageEnd); + return ( - - - +
+
+ + + { + setDefaultSortOption(option); + setSort(sortMethods[option.value]); + }} + numberEntries={numberEntries} + setNumberEntries={value => { + setNumberEntries(value); + setDisplayOffset(0); + }} + /> +
+ - - ); -} - -function SearchHeader(properties) { - const [search, setSearch] = useState(''); - - const runSearch = () => { - properties.setSearch(search.toLowerCase()); - properties.outOfBoundsHandle(0); - }; - return ( -
- { - setSearch(event.target.value); - }} - onKeyPress={event => { - if (event.key === 'Enter') { - runSearch(); - } - }} + -
); } -function FilterHeader(properties) { +function MembersFilterBar(properties) { const [filters, setFilters] = useState(undefined); + const [inputValue, setInputValue] = useState(properties.search); useEffect(() => { if (properties.filters) { @@ -257,189 +283,308 @@ function FilterHeader(properties) { } }, [properties.filters]); + const runSearch = () => properties.setSearch(inputValue.toLowerCase()); + return ( -
- Show - {filters ? ( - setInputValue(event.target.value)} + onKeyPress={event => { + if (event.key === 'Enter') runSearch(); + }} /> - ) : ( - - )} + +
+ +
+ Show + {filters ? ( + +
+ +
+ + +
+
+ ); +} + +function MembersPagination(properties) { + const limit = properties.numberEntries === 'all' ? properties.total || 1 : properties.numberEntries; + const totalPages = Math.max(1, Math.ceil((properties.total || 0) / limit)); + const currentPage = Math.floor(properties.displayOffset / limit) + 1; + + const goToPage = pageNum => { + properties.setDisplayOffset((pageNum - 1) * limit); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + if (totalPages <= 1) return null; + + const maxPagesToShow = 10; + let startPage, endPage; + if (currentPage <= 5) { + startPage = 1; + endPage = Math.min(totalPages, 5); + } else { + startPage = Math.max(1, currentPage - 5); + endPage = Math.min(totalPages, currentPage + 4); + if (endPage - startPage + 1 < maxPagesToShow) { + startPage = Math.max(1, endPage - maxPagesToShow + 1); + } } + const pageNumbers = []; + for (let index = startPage; index <= endPage; index++) pageNumbers.push(index); + + const previousEnabled = currentPage > 1; + const nextEnabled = currentPage < totalPages; + return ( - { - properties.setSort(sortMethod); - properties.setDefaultSortOption(sortOption); - }} - dataRowDisplay={member => { - var textArea = document.createElement('textarea'); - if (member.name.length > 0) { - textArea.innerHTML = member.name; - } else { - textArea.innerHTML = member.displayId; - } +
+
+
previousEnabled && goToPage(currentPage - 1)} + > + « +
+ +
+ {pageNumbers.map(num => ( +
goToPage(num)} + className={`${tableStyles.pagebubble} ${num === currentPage ? tableStyles.pagebubbleactive : ''}`} + > + {num} +
+ ))} +
+ +
nextEnabled && goToPage(currentPage + 1)} + > + » +
+
+
+ ); +} - const objectUriParts = getAfterThirdSlash(properties.uri); - const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; - const parts = properties.uri.split('/'); +function MemberTable(properties) { + const isPublicCollection = properties.isPublicCollection; + const token = useSelector(state => state.user.token); - const icon = compareUri(member.uri, `/${objectUriParts}`); + if (properties.loading) { + return ( +
+ +
+ ); + } - const removeTrailingSlash = (url) => { - return url.endsWith('/') ? url.slice(0, -1) : url; - }; + if (properties.members.length === 0) { + return
No members found
; + } - const handleIconClick = (member) => { - if (icon && icon === faTrash) { - handleDelete(member); - } else if (icon && icon === faUnlink) { - const processedUriPrefix = removeTrailingSlash(properties.processedUri); - handleUnlink(member, processedUriPrefix); // Use processedUri from props + const handleDelete = async member => { + if (member.uri && window.confirm("Would you like to remove this item from the collection?")) { + try { + await axios.get(`${publicRuntimeConfig.backend}${member.uri}/remove`, { + headers: { + "Accept": "text/plain; charset=UTF-8", + "X-authorization": token } - }; - - const handleDelete = async (member) => { - if (member.uri && window.confirm("Would you like to remove this item from the collection?")) { - try { - await axios.get(`${publicRuntimeConfig.backend}${member.uri}/remove`, { - headers: { - "Accept": "text/plain; charset=UTF-8", - "X-authorization": token - } - }); - // After successful deletion, update the state - properties.mutate(); // This will re-fetch the members - } catch (error) { - console.error('Error removing item:', error); - // Handle error appropriately - } + }); + properties.mutate(); + } catch (error) { + console.error('Error removing item:', error); + } + } + }; + + const handleUnlink = async (member, processedUriPrefix) => { + const objectUriParts = getAfterThirdSlash(properties.uri); + const objectUri = `${publicRuntimeConfig.backend}/${objectUriParts}`; + if (member.uri && window.confirm("Would you like to unlink this item from the collection?")) { + try { + await axios.post(`${objectUri}/removeMembership`, { + "member": `${processedUriPrefix}${member.uri}` + }, { + headers: { + "Accept": "text/plain; charset=UTF-8", + "X-authorization": token } - }; - - const handleUnlink = async (member, processedUri) => { - if (member.uri && window.confirm("Would you like to unlink this item from the collection?")) { - try { - await axios.post(`${objectUri}/removeMembership`, { - "member": `${processedUri}${member.uri}` - }, { - headers: { - "Accept": "text/plain; charset=UTF-8", - "X-authorization": token - } - }); - // After successful unlinking, update the state - properties.mutate(); // This will re-fetch the members - } catch (error) { - console.error('Error unlinking item:', error); - // Handle error appropriately + }); + properties.mutate(); + } catch (error) { + console.error('Error unlinking item:', error); + } + } + }; + + const removeTrailingSlash = url => (url.endsWith('/') ? url.slice(0, -1) : url); + + return ( +
+ + + + + + {!isPublicCollection && } + + + + + {properties.members.map(member => { + const objectUriParts = getAfterThirdSlash(properties.uri); + const parts = properties.uri.split('/'); + + const icon = compareUri(member.uri, `/${objectUriParts}`); + + const handleIconClick = () => { + if (icon === faTrash) { + handleDelete(member); + } else if (icon === faUnlink) { + handleUnlink(member, removeTrailingSlash(properties.processedUri)); } - } - }; - - const isShareLink = properties.uri.endsWith('/share'); - const customSuffix = isShareLink ? `/${parts.slice(-2).join('/')}` : ''; - - return ( - - - - - - {!isPublicCollection && icon === faTrash && ( - + - )} - {!isPublicCollection && icon === faUnlink && ( - - )} - - ); - }} - - /> + + {!isPublicCollection && (icon === faTrash || icon === faUnlink) && ( + + )} + + ); + })} + +
ID / NameTypeDescriptionRemove
- - - {textArea.value} - - - - - {member.displayId} - - {getType(member)}{member.description} handleIconClick(member)} className={styles.modalicon} title="Delete Member"> - + }; + + const isShareLink = properties.uri.endsWith('/share'); + const customSuffix = isShareLink ? `/${parts.slice(-2).join('/')}` : ''; + const displayType = getType(member); + const typeColor = getTypeColor(displayType); + + return ( +
+ + +
{member.displayId}
+
+ {member.name || member.displayId} +
+
+
handleIconClick(member)} className={styles.modalicon} title="Remove member from collection"> - + + {displayType && ( + + {displayType} + + )}
+
{member.description}
+
+ +
); } +const selectStyles = { + menuPortal: base => ({ ...base, zIndex: 9999 }), + control: base => ({ + ...base, + minHeight: '2.1rem', + borderRadius: '0.4rem', + borderColor: '#d5d9e0', + boxShadow: 'none', + fontSize: '0.85rem' + }) +}; + function getType(member) { var memberType = member.type ? member.type.slice(member.type.lastIndexOf('#') + 1) @@ -498,7 +643,7 @@ const useCount = (query, options, dispatch, token=null) => { ({ data, error } = useSWR([finalUrl, token, dispatch], fetcher)); const [processedData, setProcessedData] = useState(undefined); - + useEffect(() => { if (data) { processResults(data).then(result => { @@ -529,9 +674,9 @@ const useMembers = (query, options, dispatch, token=null) => { let data, error, mutate; ({ data, error, mutate } = useSWR([finalUrl, token, dispatch], fetcher)); - + const [processedData, setProcessedData] = useState(undefined); - + useEffect(() => { if (data) { processResults(data).then(result => { @@ -541,7 +686,7 @@ const useMembers = (query, options, dispatch, token=null) => { setProcessedData(undefined); } }, [data]); - + return { members: processedData, mutate @@ -567,7 +712,7 @@ const useFilters = (query, options, dispatch, token=null, privateGraph=null) => ({ data, error, mutate } = useSWR([finalUrl, token, dispatch], fetcher)); const [processedData, setProcessedData] = useState(undefined); - + useEffect(() => { if (data) { processResults(data).then(result => { @@ -621,10 +766,10 @@ const processResults = async (results) => { return Promise.all(results.results.bindings.map(async (result) => { const resultObject = {}; const currentUri = result.uri ? result.uri.value : ''; - const registriesArray = Array.isArray(registries) - ? registries + const registriesArray = Array.isArray(registries) + ? registries : Object.values(registries).filter(r => r && typeof r === 'object'); - let isExternalRegistry = currentUri && registriesArray.some(registry => + let isExternalRegistry = currentUri && registriesArray.some(registry => registry.uri && currentUri.startsWith(registry.uri) ); if (currentUri && currentUri.startsWith(localUriPrefix)) { diff --git a/frontend/styles/view.module.css b/frontend/styles/view.module.css index 1ee35c78..7e3f0db8 100644 --- a/frontend/styles/view.module.css +++ b/frontend/styles/view.module.css @@ -648,47 +648,120 @@ color: #263040; } -.memberheadercontainer { - margin: 0.5rem 0 1rem 0; +.loadinginline { + display: inline-block; +} + +.membercontrols { display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + width: 100%; } -.membersearchinput { +.membersearchbox { + display: flex; + align-items: center; + flex: 1 1 16rem; + min-width: 12rem; +} + +.membersearchboxinput { width: 100%; - padding: 0.5rem; + padding: 0.5rem 0.75rem; border-radius: 0.4rem 0 0 0.4rem; - border: none; + border: 1px solid #d5d9e0; + border-right: none; outline: none; - background-color: #e6e6e6; + font-size: 0.85rem; + background-color: #fff; } -.membername:hover { - color: #c2244b; +.membersearchboxinput:focus { + border-color: #D25627; } -.memberid:hover { - text-decoration: underline; - color: #465875; +.membersearchboxbutton { + padding: 0.5rem 0.9rem; + border: 1px solid #D25627; + border-radius: 0 0.4rem 0.4rem 0; + background-color: #D25627; + color: #fff; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; } -.loadinginline { - display: inline-block; +.membersearchboxbutton:hover { + background-color: #973d1c; + border-color: #973d1c; } -.filtercontainer { +.memberfiltergroup { display: flex; align-items: center; - width: 100%; - margin-bottom: 1rem; + gap: 0.5rem; + font-size: 0.85rem; font-weight: 500; + color: #465775; } -.filterSelect { - width: 100%; - margin-left: 0.5rem; +.memberfilterselect { + min-width: 12rem; font-weight: 400; } +.memberactionicon { + text-align: center; + color: #9aa1ab; + cursor: pointer; + transition: color 0.15s ease; +} + +.memberactionicon:hover { + color: #D25627; +} + +.memberstable { + table-layout: fixed; + margin: 0.4rem 0 1.5rem; + border: 1px solid #f4f4f4; + border-radius: 0.5rem; + box-shadow: 0 1px 2px rgba(23, 29, 38, 0.05); +} + +.memberstable thead tr:first-child th:first-child { + border-top-left-radius: 0.5rem; +} + +.memberstable thead tr:first-child th:last-child { + border-top-right-radius: 0.5rem; +} + +.memberstable tbody tr:last-child td:first-child { + border-bottom-left-radius: 0.5rem; +} + +.memberstable tbody tr:last-child td:last-child { + border-bottom-right-radius: 0.5rem; +} + +.membernamecolumn { + width: 28%; +} + +.membertypecolumn { + width: 10rem; +} + +.memberremovecolumn { + width: 3.5rem; + text-align: center; +} + .table { min-width: 100%; text-align: left; From 7378c2de239bf4f1d6fbd895710e426643003bc9 Mon Sep 17 00:00:00 2001 From: Brody Wingrove <88740722+BroD54@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:13:28 -0600 Subject: [PATCH 2/6] Restyle side panel action buttons as labeled pills --- frontend/components/Viewing/SidePanelTools.js | 94 +++++++++++-------- frontend/styles/view.module.css | 31 ++++-- 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/frontend/components/Viewing/SidePanelTools.js b/frontend/components/Viewing/SidePanelTools.js index 97ce624e..3dbffe35 100644 --- a/frontend/components/Viewing/SidePanelTools.js +++ b/frontend/components/Viewing/SidePanelTools.js @@ -177,75 +177,89 @@ export default function SidePanelTools(properties) {
- { setModal("Share"); }} - title="Share Item" // Placeholder for share button description - /> - + + Share +
+
{ setModal("Download"); }} - title="Download Item" // placeholder for download button description - /> - + + Download +
+
{ copyToast("Citation Copied!"); copyToClipboard(); }} - title="Copy Citation" // placeholder for copy button description - /> - {!isPublic && ( { - setModal("MakePublic"); - }} - title="Make Public" // placeholder for unlock button description - /> + title="Copy a citation for this item" + > + + Cite +
+ {!isPublic && ( +
{ + setModal("MakePublic"); + }} + title="Publish this item to the public collection" + > + + Make Public +
)} {isPublic && isCollection && loggedIn && ( - { setModal("CollectionIcon"); }} - title="Change Collection Icon" - /> + title="Change this collection's icon" + > + + Icon + )} {loggedIn && ( - { setModal("AddToCollection"); }} - title="Add to Collection" // placeholder for add to collection button description - /> + title="Add this item to one of your collections" + > + + Add to Collection + )} {isOwner && ( - setModal("Delete")} - title="Delete Item" // placeholder for delete button description - uri={properties.url} - /> + title="Delete this item" + > + + Delete + )} diff --git a/frontend/styles/view.module.css b/frontend/styles/view.module.css index 7e3f0db8..de8277e8 100644 --- a/frontend/styles/view.module.css +++ b/frontend/styles/view.module.css @@ -201,9 +201,10 @@ } .subheader { - margin: 0.5rem 1rem; + margin: 0.5rem 1rem 0.75rem 1rem; display: flex; - justify-content: space-between; + flex-direction: column; + gap: 0.5rem; } .id { @@ -219,16 +220,32 @@ color: #263040; } +.actionicons { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + .actionicon { - margin-left: 0.4rem; - color: #D25627; - font-size: 1.2rem; + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.65rem; + font-size: 0.78rem; + font-weight: 500; + color: #fff; + background-color: #D25627; + border-radius: 0.3rem; cursor: pointer; - transition: transform .2s; + transition: background-color .15s ease; } .actionicon:hover { - transform: scale(1.3); + background-color: #973d1c; +} + +.actioniconlabel { + white-space: nowrap; } .infocontainer { From aa3caa3560a6df119cec34d26a6382465ca9aa33 Mon Sep 17 00:00:00 2001 From: Brody Wingrove <88740722+BroD54@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:14:28 -0600 Subject: [PATCH 3/6] Make object metadata and page-section nav compact and scannable --- frontend/components/Viewing/MetadataInfo.js | 156 ++++++--------- frontend/styles/view.module.css | 198 ++++++++++++++------ 2 files changed, 202 insertions(+), 152 deletions(-) diff --git a/frontend/components/Viewing/MetadataInfo.js b/frontend/components/Viewing/MetadataInfo.js index 73dc22e1..61dc044a 100644 --- a/frontend/components/Viewing/MetadataInfo.js +++ b/frontend/components/Viewing/MetadataInfo.js @@ -14,7 +14,6 @@ const { publicRuntimeConfig } = getConfig(); export default function MetadataInfo({ title, link, label, icon, specific, uri }) { const theme = JSON.parse(localStorage.getItem('theme')) || {}; - const [isHovered, setIsHovered] = useState(false); const [isEditing, setIsEditing] = useState(false); const [newMetadata, setNewMetadata] = useState(''); @@ -184,10 +183,6 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } }); }; - const hoverStyle = { - backgroundColor: isHovered ? (theme?.hoverColor || '#00A1E4') : (theme?.themeParameters?.[0]?.value || styles.infoheader.backgroundColor) - }; - const handleAddMetadata = (label) => { const editedText = newMetadata.trim(); @@ -280,22 +275,13 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } const renderedLabel = (
{label} - {(label === "Source" || label === "Type" || label === "Role") && ( - <> - {/* Add space */} - { - isOwner && ( - <> - setIsEditing(true)} - className={styles.plusIcon} - title="Add a new source" - /> - - ) - } - + {(label === "Source" || label === "Type" || label === "Role") && isOwner && ( + setIsEditing(true)} + className={styles.plusIcon} + title="Add a new source" + /> )}
); @@ -321,10 +307,8 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } return parts.map(part => part.replace(new RegExp(commaPlaceholder, 'g'), ',')); } const renderedTitle = ( -
-
-
- {metadata.map((data, index) => { +
+ {metadata.map((data, index) => { let isEditingThisItem = false; let currentValue = ""; let correspondingLink = (label === 'Source' || label === 'Type' || label === 'Role') @@ -351,9 +335,8 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } break; } return ( -
-
- {isEditingThisItem ? ( + + {isEditingThisItem ? (
) : ( - - {processedSource} - - )} -
- {(label === "Source" || label === "Type" || label === "Role") && data && ( - isEditingThisItem ? null : ( -
- {isOwner && ( - <> - - - - )} -
- ) + <> + + {processedSource} + + {(label === "Source" || label === "Type" || label === "Role") && data && isOwner && ( + + handleEditMetadata(index, data, label)} /> + handleDeleteMetadata(e, data, label)} /> + + )} + )} -
-
+ ); })} -
-
); @@ -421,47 +392,42 @@ export default function MetadataInfo({ title, link, label, icon, specific, uri } // Rendered section including the header and the title (without link) const renderedSection = ( -
-
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
- {!specific ? ( - - ) : ( - - )} -
- {renderedLabel} +
+
+ {!specific ? ( + + ) : ( + + )}
- {renderedTitle} - {isEditing ? ( -
- setNewMetadata(e.target.value)} - /> - - + -
- ) : null} + style={{ + backgroundColor: theme?.themeParameters?.[0]?.value || '#333', // Use theme color or default to #333 + color: theme?.themeParameters?.[1]?.value || '#fff', // Use text color from theme or default to #fff + }} + >Cancel +
+ ) : null} +
); return renderedSection; diff --git a/frontend/styles/view.module.css b/frontend/styles/view.module.css index de8277e8..a54e53a6 100644 --- a/frontend/styles/view.module.css +++ b/frontend/styles/view.module.css @@ -250,82 +250,131 @@ .infocontainer { margin: 0 auto; - width: 19rem; - padding: 1rem 0; + width: 100%; + padding: 0.25rem 1rem 0.5rem 1rem; + box-sizing: border-box; } -.info { - margin: 0 auto; - overflow-wrap: break-word; +.metarow { display: flex; - flex-direction: column; - align-items: center; + align-items: flex-start; + gap: 0.5rem; + padding: 0.4rem 0.25rem; + border-bottom: 1px solid #e2e2e2; + overflow-wrap: break-word; } -.info:hover .infoheader { - background-color: #00A1E4; +.metarow:last-child { + border-bottom: none; } -.info:hover .infotitle { - border: 1px solid #00A1E4; +.metarow:hover { + background-color: #f0f0f0; } -.infoheader { +.metarowbody { + flex: 1; + min-width: 0; +} + +.infolabel { display: flex; - justify-content: space-between; align-items: center; - overflow-wrap: break-word; - background-color: #465875; - color: #fff; - padding: 0.2rem 0.5rem; - width: 95%; + gap: 0.4rem; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; + color: #888; +} + +.infoicon { + font-size: 0.85rem; + color: #465875; +} + +.infoiconcontainer { + flex-shrink: 0; + width: 1.1rem; + margin-top: 0.15rem; + text-align: center; +} + +.plusIcon { cursor: pointer; + color: #D25627; + font-size: 0.75rem; + opacity: 0; + transition: opacity .15s ease; +} + +.metarow:hover .plusIcon { + opacity: 1; } -.infogeneric { +.infotitle, +.infotitlegeneric { display: flex; - justify-content: space-between; align-items: center; + gap: 0.4rem; + flex-wrap: wrap; + margin: 0; + padding: 0; + font-weight: 400; + color: #222; + font-size: 0.85rem; overflow-wrap: break-word; - background-color: #D25627; - color: #fff; - padding: 0.2rem 0.5rem; - width: 95%; + border: none; + width: 100%; } -.infolabel { - font-size: 0.8rem; - font-weight: 500; +.infotitle a, +.infotitlegeneric a { + color: #222; + text-decoration: none; } -.infoicon { - font-size: 1rem; +.infotitle a:hover, +.infotitlegeneric a:hover { + color: #D25627; + text-decoration: underline; } -.infoiconcontainer { - width: 1rem; +.metarowactions { + display: inline-flex; + align-items: center; + gap: 0.35rem; + opacity: 0; + transition: opacity .15s ease; } -.infotitle { - padding: 0.3rem; - margin: 0; - font-weight: 500; - color: #000; - font-size: 0.8rem; - overflow-wrap: break-word; - border: 1px solid #465875; - width: 95%; +.metarow:hover .metarowactions { + opacity: 1; } -.infotitlegeneric { - padding: 0.3rem; - margin: 0; - font-weight: 500; - color: #000; +.metarowactions svg { + cursor: pointer; + color: #9aa1ab; + font-size: 0.75rem; +} + +.metarowactions svg:hover { + color: #D25627; +} + +.addMetadata { + display: flex; + gap: 0.4rem; + margin-top: 0.3rem; +} + +.addMetadata input { + flex: 1; + min-width: 0; + padding: 0.3rem 0.5rem; + border: 1px solid #d5d9e0; + border-radius: 0.3rem; font-size: 0.8rem; - overflow-wrap: break-word; - border: 1px solid #D25627; - width: 95%; } .pagesections { @@ -357,24 +406,26 @@ } .sectionheader { - background-color: #D25627; - padding: 0.75rem; + background-color: #fff; + border: 1px solid #e2e2e2; + padding: 0.55rem 0.75rem; border-radius: 0.4rem; - color: #fff; + color: #2d333b; display: flex; align-items: center; justify-content: space-between; cursor: pointer; - transition: transform .1s; - box-shadow: 0px 0px 5px #7e7e7e; + transition: background-color .15s ease, border-color .15s ease; + box-shadow: none; } .sectionheaderparent { - margin-bottom: 0.5rem; + margin-bottom: 0.35rem; } .sectionheader:hover { - transform: scale(1.03); + background-color: #fdf1ec; + border-color: #D25627; } .titleandbox { @@ -388,8 +439,9 @@ } .sectionheadericon { - font-size: 1.3rem; + font-size: 1.1rem; margin-right: 0.5rem; + color: #D25627; } .content { @@ -820,10 +872,43 @@ .otherPropertiesTable { overflow: visible; + border-collapse: collapse; + width: 100%; +} + +.otherPropertiesTable tr:nth-child(odd) { + background-color: transparent; +} + +.otherPropertiesTable tr { + border-bottom: 1px solid #eef0f3; +} + +.otherPropertiesTable tr:last-child { + border-bottom: none; +} + +.otherPropertiesTable tr:hover { + background-color: #f7f7f7; } .otherPropertiesTable td { overflow: visible; + padding: 0.45rem 0.4rem; + border-top: none; + vertical-align: middle; + font-size: 0.85rem; +} + +.otherPropertiesTable .editIcon, +.otherPropertiesTable .searchicon { + opacity: 0; + transition: opacity .15s ease; +} + +.otherPropertiesTable tr:hover .editIcon, +.otherPropertiesTable tr:hover .searchicon { + opacity: 1; } .searchicon { @@ -1085,7 +1170,6 @@ .nosectionheadershadow { box-shadow: none; - background-color: #D25627; cursor: default; } From 7e5b73ce913b127642162a47e0b35ac957b2e562 Mon Sep 17 00:00:00 2001 From: Brody Wingrove <88740722+BroD54@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:32:49 -0600 Subject: [PATCH 4/6] Remove SBOL language in other properties and "Show ___" filter --- .../Search/AdvancedSearch/FacetOptionList.js | 12 +----------- .../Search/StandardSearch/SelectedFilters.js | 2 +- frontend/components/Viewing/Collection/Members.js | 3 ++- .../components/Viewing/Sections/OtherProperties.js | 3 ++- frontend/utilities/condenseLabel.js | 10 ++++++++++ 5 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 frontend/utilities/condenseLabel.js diff --git a/frontend/components/Search/AdvancedSearch/FacetOptionList.js b/frontend/components/Search/AdvancedSearch/FacetOptionList.js index 525001d1..3b26c373 100644 --- a/frontend/components/Search/AdvancedSearch/FacetOptionList.js +++ b/frontend/components/Search/AdvancedSearch/FacetOptionList.js @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState } from 'react'; import styles from '../../../styles/advancedsearch.module.css'; +import { condenseLabel } from '../../../utilities/condenseLabel'; import { getFacetDotColor } from '../../../utilities/facetColor'; import Loading from '../../Reusable/MiniLoading'; @@ -18,17 +19,6 @@ export const toggleOption = (value, isMulti, option) => { : [...value, option]; }; -// drops the "prefix:" and/or "category/" from a short name for display -// (so:engineered_region -> engineered_region, partType/RBS -> RBS) -export const condenseLabel = label => { - const afterPrefix = label.includes(':') - ? label.slice(label.lastIndexOf(':') + 1) - : label; - return afterPrefix.includes('/') - ? afterPrefix.slice(afterPrefix.lastIndexOf('/') + 1) - : afterPrefix; -}; - const getVisibleOptions = (data, search) => search ? data.filter(option => diff --git a/frontend/components/Search/StandardSearch/SelectedFilters.js b/frontend/components/Search/StandardSearch/SelectedFilters.js index 5993b11f..e0413dc5 100644 --- a/frontend/components/Search/StandardSearch/SelectedFilters.js +++ b/frontend/components/Search/StandardSearch/SelectedFilters.js @@ -1,7 +1,7 @@ import { shortName } from '../../../namespace/namespace'; import styles from '../../../styles/standardsearch.module.css'; +import { condenseLabel } from '../../../utilities/condenseLabel'; import { getFacetDotColor } from '../../../utilities/facetColor'; -import { condenseLabel } from '../AdvancedSearch/FacetOptionList'; const buildSingleChips = properties => { const filters = [ diff --git a/frontend/components/Viewing/Collection/Members.js b/frontend/components/Viewing/Collection/Members.js index 0cb05495..285ff409 100644 --- a/frontend/components/Viewing/Collection/Members.js +++ b/frontend/components/Viewing/Collection/Members.js @@ -20,6 +20,7 @@ import loadTemplate from '../../../sparql/tools/loadTemplate'; import { shortName } from '../../../namespace/namespace'; import lookupRole from '../../../namespace/lookupRole'; import { getTypeColor } from '../../../utilities/typeColor'; +import { condenseLabel } from '../../../utilities/condenseLabel'; import Link from 'next/link'; import { addError, logoutUser } from '../../../redux/actions'; import { processUrl } from '../../Admin/Registries'; @@ -268,7 +269,7 @@ function MembersFilterBar(properties) { if (properties.filters) { const newFilters = properties.filters.map(filter => { const shortNamedFilter = shortName(filter.uri); - return { value: filter.uri, label: shortNamedFilter }; + return { value: filter.uri, label: condenseLabel(shortNamedFilter) }; }); newFilters.sort((a, b) => (a.label > b.label ? 1 : -1)); newFilters.unshift({ diff --git a/frontend/components/Viewing/Sections/OtherProperties.js b/frontend/components/Viewing/Sections/OtherProperties.js index 0b3826e3..37ddbb0a 100644 --- a/frontend/components/Viewing/Sections/OtherProperties.js +++ b/frontend/components/Viewing/Sections/OtherProperties.js @@ -8,6 +8,7 @@ import { getAfterThirdSlash } from '../ViewHeader'; import { isUriOwner, isValidURI } from '../Shell'; import Loading from '../../Reusable/Loading'; import Link from 'next/link'; +import lookupRole from '../../../namespace/lookupRole'; import styles from '../../../styles/view.module.css'; import { useDispatch, useSelector } from 'react-redux'; @@ -239,7 +240,7 @@ export default function OtherProperties(properties) { return ( - {name.split('/').pop()} + {lookupRole(name).term || name} engineered_region, partType/RBS -> RBS) +export const condenseLabel = label => { + const afterPrefix = label.includes(':') + ? label.slice(label.lastIndexOf(':') + 1) + : label; + return afterPrefix.includes('/') + ? afterPrefix.slice(afterPrefix.lastIndexOf('/') + 1) + : afterPrefix; +}; From 50e1290c01c329de4472ce7e6ca66e4675284543 Mon Sep 17 00:00:00 2001 From: Brody Wingrove <88740722+BroD54@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:34:35 -0600 Subject: [PATCH 5/6] fix member.description to render as HTML --- .../components/Viewing/Collection/Members.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/components/Viewing/Collection/Members.js b/frontend/components/Viewing/Collection/Members.js index 285ff409..9ad61508 100644 --- a/frontend/components/Viewing/Collection/Members.js +++ b/frontend/components/Viewing/Collection/Members.js @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import useSWR from 'swr'; import axios from 'axios'; import Select from 'react-select'; +import showdown from 'showdown'; import getConfig from 'next/config'; const { publicRuntimeConfig } = getConfig(); @@ -42,6 +43,8 @@ const sortOptions = [ { value: 'displayId', label: 'Identifier' } ]; +const sdconverter = new showdown.Converter(); + export default function Members(properties) { const token = useSelector(state => state.user.token); const privateGraph = useSelector(state => state.user.graphUri); @@ -216,6 +219,17 @@ export default function Members(properties) { : Math.min(processedMembers.length, pageStart + numberEntries); const pageMembers = processedMembers.slice(pageStart, pageEnd); + useEffect(() => { + if ( + displayOffset > 0 && + processedMembers.length > 0 && + pageStart >= processedMembers.length + ) { + const limit = numberEntries === 'all' ? processedMembers.length : numberEntries; + setDisplayOffset(previous => Math.max(0, previous - limit)); + } + }, [processedMembers, pageStart]); + return (
@@ -555,7 +569,10 @@ function MemberTable(properties) { )} -
{member.description}
+
{!isPublicCollection && (icon === faTrash || icon === faUnlink) && ( Date: Thu, 17 Sep 2026 12:56:41 -0600 Subject: [PATCH 6/6] fix alignment issue and box-within-box feel in Members --- .../components/Viewing/Collection/Members.js | 16 ++++++++-------- frontend/styles/resulttable.module.css | 2 +- frontend/styles/view.module.css | 19 ------------------- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/frontend/components/Viewing/Collection/Members.js b/frontend/components/Viewing/Collection/Members.js index 9ad61508..8d944b61 100644 --- a/frontend/components/Viewing/Collection/Members.js +++ b/frontend/components/Viewing/Collection/Members.js @@ -516,8 +516,8 @@ function MemberTable(properties) { ID / Name - Type Description + Type {!isPublicCollection && Remove} @@ -549,11 +549,17 @@ function MemberTable(properties) {
{member.displayId}
- {member.name || member.displayId} + {member.name || member.displayId}
+ +
+ {displayType && ( )} - -
- {!isPublicCollection && (icon === faTrash || icon === faUnlink) && (