From 05a3ac5831bd48fd599972c3d7f61548e8d2d1c1 Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Tue, 23 Sep 2025 00:18:55 +0200 Subject: [PATCH 1/2] fixes discussed at the last sprint call --- src/components/Auth/Login.jsx | 11 ++- src/components/CurieEditor/CuriesTabPanel.jsx | 56 ++++++------- src/components/CurieEditor/index.jsx | 15 +++- .../EditBulkTerms/EditBulkTermsDialog.jsx | 12 ++- .../Dashboard/EditBulkTerms/SearchTerms.jsx | 1 + src/components/Header/index.jsx | 21 +++++ .../SearchResults/SearchResultsBox.jsx | 34 ++------ .../AddNewOntologyDialog.jsx | 64 ++++++++++++++- src/components/SingleOrganization/index.jsx | 50 +++++++++-- .../SingleTermView/OntologySearch.jsx | 21 +++-- src/components/SingleTermView/index.jsx | 28 +------ src/components/TermEditor/ImportFile.jsx | 4 +- src/components/common/CustomButtonGroup.jsx | 4 +- src/components/common/CustomTableHead.jsx | 3 +- src/components/common/CustomViewButton.jsx | 68 ++++++++++----- .../common/FeatureNotAvailableDialog.jsx | 82 +++++++++++++++++++ .../common/FeatureNotAvailableDialog.md | 76 +++++++++++++++++ src/components/common/OrganizationsList.jsx | 75 +++++++++++------ src/helpers/useOrganizations.ts | 42 +++++++--- src/utils.js | 8 +- 20 files changed, 503 insertions(+), 172 deletions(-) create mode 100644 src/components/common/FeatureNotAvailableDialog.jsx create mode 100644 src/components/common/FeatureNotAvailableDialog.md diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index 91771284..c29181ce 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -230,6 +230,13 @@ const Login = () => { } }; + const handleFormSubmit = (e) => { + e.preventDefault(); + if (formData.username.trim() && formData.password.trim()) { + loginUser(); + } + }; + return ( {isLoading ? @@ -243,7 +250,7 @@ const Login = () => { Log in to your account Welcome! Please enter your details. {errors.auth && {errors.auth}} -
+ { - diff --git a/src/components/CurieEditor/CuriesTabPanel.jsx b/src/components/CurieEditor/CuriesTabPanel.jsx index c73abeea..7ad92f60 100644 --- a/src/components/CurieEditor/CuriesTabPanel.jsx +++ b/src/components/CurieEditor/CuriesTabPanel.jsx @@ -32,29 +32,33 @@ const fieldStyle = { } }; -const tableCellStyle = { - minWidth: '37.5rem', +const tableCellBaseStyle = { color: gray600, fontWeight: 400 }; +const prefixCellStyle = { + ...tableCellBaseStyle, + width: '25%' +}; + +const namespaceCellStyle = { + ...tableCellBaseStyle, + width: '75%' +}; + const CuriesTabPanel = (props) => { const { curieValue, error, loading, rows, editMode, onCurieAmountChange, onAddRow, onDeleteRow, onChangeRow } = props; const [rowIndex, setRowIndex] = React.useState(-1); const [columnIndex, setColumnIndex] = React.useState(-1); const [order, setOrder] = React.useState('asc'); const [orderBy, setOrderBy] = React.useState('prefix'); - const [sortTriggered, setSortTriggered] = React.useState(false); const sortedRows = React.useMemo(() => { - // Ensure rows is always an array + // Ensure rows is always an array and apply natural sorting by default const safeRows = Array.isArray(rows) ? rows : []; - - if (sortTriggered) { - return stableSort(safeRows, getComparator(order, orderBy)); - } - return safeRows; - }, [rows, order, orderBy, sortTriggered]); + return stableSort(safeRows, getComparator(order, orderBy)); + }, [rows, order, orderBy]); React.useEffect(() => { onCurieAmountChange?.(rows.length) @@ -65,12 +69,6 @@ const CuriesTabPanel = (props) => { setColumnIndex(-1); } - const handleSort = (dir) => { - setSortTriggered(true); - setOrder(dir); - handleExit(); - } - if (error) { return
error
; } @@ -84,17 +82,28 @@ const CuriesTabPanel = (props) => { rows={rows} order={order} orderBy={orderBy} - setOrder={handleSort} + setOrder={setOrder} setOrderBy={setOrderBy} headCells={editMode ? headCellsEditMode : headCells} > + {editMode && ( + + onAddRow(curieValue)}> + + + + + + + + )} {Array.isArray(sortedRows) && sortedRows.map((row, index) => { return ( { setRowIndex(index); setColumnIndex(0); }} - sx={{ border: rowIndex === index && columnIndex === 0 && editMode ? `2px solid ${brand500} !important` : 'inherit', ...tableCellStyle }} + sx={{ border: rowIndex === index && columnIndex === 0 && editMode ? `2px solid ${brand500} !important` : 'inherit', ...prefixCellStyle }} > { rowIndex === index && columnIndex === 0 && editMode ? @@ -115,7 +124,7 @@ const CuriesTabPanel = (props) => { { setRowIndex(index); setColumnIndex(1); }} - sx={{ border: rowIndex === index && columnIndex === 1 && editMode ? `2px solid ${brand500} !important` : 'inherit', ...tableCellStyle }} + sx={{ border: rowIndex === index && columnIndex === 1 && editMode ? `2px solid ${brand500} !important` : 'inherit', ...namespaceCellStyle }} > { rowIndex === index && columnIndex === 1 && editMode ? @@ -143,15 +152,6 @@ const CuriesTabPanel = (props) => { ); })} - {editMode && ( - - onAddRow(curieValue)}> - - - - - - )} )} diff --git a/src/components/CurieEditor/index.jsx b/src/components/CurieEditor/index.jsx index 77d4430f..4e72e5fb 100644 --- a/src/components/CurieEditor/index.jsx +++ b/src/components/CurieEditor/index.jsx @@ -75,8 +75,15 @@ const CurieEditor = () => { }); data = transformCuriesResponse(response); } else if (type === 'latest') { - // "Latest" tab - stay empty for now - data = []; + // "Latest" tab - get curies from "base" groupname (same as curated) + const response = await getOrganizationsCuries('base').catch(error => { + if (error?.response?.status === 501) { + console.warn('Curies endpoint not implemented yet (501) for base, using empty array'); + return [{}]; // Return array with empty object to match expected structure + } + throw error; + }); + data = transformCuriesResponse(response); } setCuries(prev => ({ ...prev, [type]: data })); @@ -91,7 +98,7 @@ const CurieEditor = () => { }, [user]); const handleAddNewCurieRow = (curieValue) => { - setCuries(prev => ({ ...prev, [curieValue]: [...prev[curieValue], newRowObj] })); + setCuries(prev => ({ ...prev, [curieValue]: [newRowObj, ...prev[curieValue]] })); }; const handleDeleteCurieRow = (curieValue, rowPrefix, rowNamespace) => { @@ -174,7 +181,7 @@ const CurieEditor = () => { curieValue={tab} error={error} loading={loading} - editMode + editMode={tab === 'base'} // Only allow editing for 'base' (My curies) tab rows={curies[tab]} onCurieAmountChange={handleCurieAmountChange} onAddRow={handleAddNewCurieRow} diff --git a/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx b/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx index 513345bd..b0373b65 100644 --- a/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx +++ b/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useContext, useEffect } from "react"; import PropTypes from "prop-types"; import EditTerms from "./EditTerms"; import SearchTerms from "./SearchTerms"; @@ -12,6 +12,7 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import SearchTermsData from "../../../static/SearchTermsData.json"; import { patchTerm } from "../../../api/endpoints"; +import { GlobalDataContext } from "../../../contexts/DataContext"; const initialSearchConditions = { attribute: '', value: '', condition: 'where', relation: SearchTermsData.objectOptions[0].value } @@ -70,6 +71,7 @@ HeaderRightSideContent.propTypes = { }; const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) => { + const { activeOntology } = useContext(GlobalDataContext); const [searchConditions, setSearchConditions] = useState([initialSearchConditions]); const [ontologyTerms, setOntologyTerms] = useState([]); const [ontologyAttributes, setOntologyAttributes] = useState([]); @@ -78,6 +80,14 @@ const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) = const [originalTerms, setOriginalTerms] = useState([]); const [batchUpdateResults, setBatchUpdateResults] = useState(null); const [isUpdating, setIsUpdating] = useState(false); + + // Prefill selectedOntology with activeOntology when dialog opens + useEffect(() => { + if (open && activeOntology && !selectedOntology) { + setSelectedOntology(activeOntology); + } + }, [open, activeOntology, selectedOntology]); + const performBatchUpdate = async (termsToUpdate = null) => { setIsUpdating(true); diff --git a/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx b/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx index 59af63e5..cc45ede1 100644 --- a/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx +++ b/src/components/Dashboard/EditBulkTerms/SearchTerms.jsx @@ -299,6 +299,7 @@ const SearchTerms = ({ searchConditions, setSearchConditions, initialSearchCondi extra={ontologySearchExtraStyles} userGroupname={user?.groupname} onOntologySelect={handleOntologySelect} + disableGlobalUpdate={true} /> {selectedOntology && ontologyTerms.length === 0 && !attributesLoading && ( diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 3e443907..f5373932 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -37,7 +37,9 @@ import { useCookies } from 'react-cookie'; import CustomButtonGroup from '../common/CustomButtonGroup'; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import AddIcon from '@mui/icons-material/Add'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; import AddNewTermDialog from '../TermEditor/newTerm/AddNewTermDialog'; +import AddNewOntologyDialog from '../SingleOrganization/AddNewOntologyDialog'; import { vars } from "../../theme/variables"; const { gray200, white, gray100, gray600 } = vars; @@ -151,6 +153,7 @@ const Header = () => { const [isLoggedIn, setIsLoggedIn] = React.useState(false); const { user, setUserData } = useContext(GlobalDataContext); const [openNewTermDialog, setOpenNewTermDialog] = React.useState(false); + const [openNewOntologyDialog, setOpenNewOntologyDialog] = React.useState(false); // eslint-disable-next-line no-unused-vars const [existingCookies, setCookie, removeCookie] = useCookies(['session']); @@ -179,6 +182,14 @@ const Header = () => { setOpenNewTermDialog(true); } + const handleNewOntologyDialogClose = () => { + setOpenNewOntologyDialog(false); + } + + const handleNewOntologyDialogOpen = () => { + setOpenNewOntologyDialog(true); + } + // eslint-disable-next-line no-unused-vars const handleSetUserData = (user, organization) => { setUserData(user, organization); @@ -293,6 +304,11 @@ const Header = () => { icon: , action: handleNewTermDialogOpen }, + { + label: 'Add a new ontology', + icon: , + action: handleNewOntologyDialogOpen + }, { label: 'Bulk add terms', icon: , @@ -471,6 +487,11 @@ const Header = () => { )} + ) diff --git a/src/components/SearchResults/SearchResultsBox.jsx b/src/components/SearchResults/SearchResultsBox.jsx index fbd5d747..f6d05d05 100644 --- a/src/components/SearchResults/SearchResultsBox.jsx +++ b/src/components/SearchResults/SearchResultsBox.jsx @@ -4,32 +4,15 @@ import PropTypes from 'prop-types'; import { TableChartIcon, ListIcon } from '../../Icons'; import OntologySearch from '../SingleTermView/OntologySearch'; import CustomSingleSelect from '../common/CustomSingleSelect'; -import { Box, Typography, Grid, ButtonGroup, Button, Stack, Divider } from '@mui/material'; +import CustomViewButton from '../common/CustomViewButton'; +import { Box, Typography, Grid, ButtonGroup, Stack, Divider } from '@mui/material'; import CustomPagination from '../common/CustomPagination'; import { vars } from '../../theme/variables'; import { GlobalDataContext } from '../../contexts/DataContext'; -const { gray50, gray200, gray300, gray600 } = vars; - -const CustomViewButton = ({ view, listView, onClick, icon }) => ( - -); +const { gray200, gray600 } = vars; + + const getPaginationSettings = (totalItems) => { const largeDatasetOptions = [20, 50, 100, 200]; @@ -170,13 +153,6 @@ const SearchResultsBox = ({ ); }; -CustomViewButton.propTypes = { - view: PropTypes.string, - listView: PropTypes.string, - onClick: PropTypes.func, - icon: PropTypes.node -}; - SearchResultsBox.propTypes = { allResults: PropTypes.object, pageResults: PropTypes.object, diff --git a/src/components/SingleOrganization/AddNewOntologyDialog.jsx b/src/components/SingleOrganization/AddNewOntologyDialog.jsx index b336048f..b8655688 100644 --- a/src/components/SingleOrganization/AddNewOntologyDialog.jsx +++ b/src/components/SingleOrganization/AddNewOntologyDialog.jsx @@ -145,8 +145,8 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization data: null }; - // Process JSON, JSON-LD, and CSV files - if (file.name.endsWith('.json') || file.name.endsWith('.jsonld') || file.name.endsWith('.csv')) { + // Process JSON, JSON-LD, CSV, and TTL files + if (file.name.endsWith('.json') || file.name.endsWith('.jsonld') || file.name.endsWith('.csv') || file.name.endsWith('.ttl')) { try { const text = await new Promise((resolve, reject) => { const reader = new FileReader(); @@ -187,6 +187,64 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization }); } } + } else if (file.name.endsWith('.ttl')) { + // Handle TTL (Turtle) format + const lines = text.split('\n').filter(line => line.trim()); + + // Extract title from comments or @base/@prefix declarations + for (const line of lines) { + const trimmedLine = line.trim(); + + // Look for title in comments + if (trimmedLine.startsWith('#') && + (trimmedLine.toLowerCase().includes('title:') || + trimmedLine.toLowerCase().includes('name:'))) { + const match = trimmedLine.match(/(?:title|name):\s*(.+)/i); + if (match && !title) { + title = match[1].trim(); + } + } + + // Look for rdfs:label or dc:title + if (trimmedLine.includes('rdfs:label') || trimmedLine.includes('dc:title')) { + const match = trimmedLine.match(/(?:rdfs:label|dc:title)\s+["']([^"']+)["']/); + if (match && !title) { + title = match[1].trim(); + } + } + } + + // Extract subjects from RDF type declarations + for (const line of lines) { + const trimmedLine = line.trim(); + + // Look for 'a' (rdf:type) declarations + if (trimmedLine.includes(' a ') && !trimmedLine.startsWith('#')) { + const match = trimmedLine.match(/\s+a\s+([^;\s.]+)/); + if (match) { + const subject = match[1].trim(); + if (subject && !subjects.includes(subject)) { + subjects.push(subject); + } + } + } + + // Look for rdf:type declarations + if (trimmedLine.includes('rdf:type') && !trimmedLine.startsWith('#')) { + const match = trimmedLine.match(/rdf:type\s+([^;\s.]+)/); + if (match) { + const subject = match[1].trim(); + if (subject && !subjects.includes(subject)) { + subjects.push(subject); + } + } + } + } + + // If no title found, try to extract from filename + if (!title) { + title = file.name.replace('.ttl', '').replace(/[-_]/g, ' '); + } } else { // Handle JSON and JSON-LD formats const jsonData = JSON.parse(text); @@ -314,7 +372,7 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization message={newOntologyResponse?.description} subMessage={newOntologyResponse?.message} addButtonTitle={"Add a new ontology"} - finishButtonTitle={"Go to ontology"} + finishButtonTitle={"Close"} open={openStatusDialog} handleClose={handleCloseStatusDialog} handleCloseandAdd={handleFinishButtonClick} diff --git a/src/components/SingleOrganization/index.jsx b/src/components/SingleOrganization/index.jsx index 77427ff8..c64eb41a 100644 --- a/src/components/SingleOrganization/index.jsx +++ b/src/components/SingleOrganization/index.jsx @@ -3,6 +3,7 @@ import { Box, Button, ButtonGroup, + Chip, Divider, Grid, Stack, @@ -35,7 +36,9 @@ import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutl // TODO: These API endpoints are currently returning 501 (Not Implemented) errors // They have been updated to use real API service instead of mock data // Error handling is in place to gracefully handle 501 responses -import { getOrganizationsCuries, getOrganizationsTerms, getOrganizationsOntologies } from "../../api/endpoints/apiService"; +import { getOrganizationsCuries, getOrganizationsTerms, getOrganizationsOntologies, getOrganizations } from "../../api/endpoints/apiService"; +import { GlobalDataContext } from "../../contexts/DataContext"; +import { useContext } from "react"; import { vars } from "../../theme/variables"; const { gray25, gray200, gray500, gray600 } = vars; @@ -62,7 +65,9 @@ const useOrganizationData = (id) => { const [organizationCuries, setOrganizationCuries] = useState([]); const [organizationTerms, setOrganizationTerms] = useState([]); const [organizationOntologies, setOrganizationOntologies] = useState([]); + const [userRole, setUserRole] = useState(null); const [loading, setLoading] = useState(false); + const { user } = useContext(GlobalDataContext); useEffect(() => { const fetchData = async () => { @@ -122,6 +127,23 @@ const useOrganizationData = (id) => { } setOrganizationOntologies(ontologiesRes || []); + + // Fetch user role for this organization + if (user?.groupname) { + try { + const organizationsResponse = await getOrganizations(user.groupname); + if (organizationsResponse.length > 0) { + // organizationsResponse is an array of [role, orgName] pairs + const currentOrgData = organizationsResponse.find((orgData) => orgData[1] === id); + if (currentOrgData) { + setUserRole(currentOrgData[0]); // role is the first element + } + } + } catch (roleError) { + console.warn("Could not fetch user role for organization:", roleError); + setUserRole(null); + } + } } catch (error) { console.error("Error fetching organization data", error); // Set empty data on error to prevent UI issues @@ -129,6 +151,7 @@ const useOrganizationData = (id) => { setOrganizationTerms([]); setOrganizationCuries([]); setOrganizationOntologies([]); + setUserRole(null); } finally { setLoading(false); } @@ -137,7 +160,7 @@ const useOrganizationData = (id) => { console.log('useOrganizationData: Fetching data for organization:', id); fetchData(); } - }, [id]); + }, [id, user?.groupname]); // Function to refresh only ontologies const refreshOntologies = async () => { @@ -157,7 +180,7 @@ const useOrganizationData = (id) => { } }; - return { organization, organizationCuries, organizationTerms, organizationOntologies, loading, refreshOntologies }; + return { organization, organizationCuries, organizationTerms, organizationOntologies, userRole, loading, refreshOntologies }; }; const SingleOrganization = () => { @@ -178,7 +201,7 @@ const SingleOrganization = () => { const navigate = useNavigate(); const { title } = useParams(); // Get organization name from URL params - const { organization, organizationTerms, organizationOntologies, loading, refreshOntologies } = useOrganizationData(title); + const { organization, organizationTerms, organizationOntologies, userRole, loading, refreshOntologies } = useOrganizationData(title); useEffect(() => { if (Array.isArray(organizationTerms) && organizationTerms.length > 0) { @@ -272,9 +295,22 @@ const SingleOrganization = () => { - - {organization?.name || title} - + + + {organization?.name || title} + + {userRole && ( + + )} + - - + /> ) } diff --git a/src/components/TermEditor/ImportFile.jsx b/src/components/TermEditor/ImportFile.jsx index 2ebe06a2..60ba8027 100644 --- a/src/components/TermEditor/ImportFile.jsx +++ b/src/components/TermEditor/ImportFile.jsx @@ -113,7 +113,7 @@ const ImportFile = ({ onFilesSelected }) => { hidden id="browse" onChange={handleFileChange} - accept=".csv,.json,.jsonld" + accept=".csv,.json,.jsonld,.ttl" multiple /> @@ -122,7 +122,7 @@ const ImportFile = ({ onFilesSelected }) => { or drag and drop - CSV, JSON, JSON-LD (max. 800MB) + CSV, JSON, JSON-LD, TTL (max. 800MB) {error && {error}} diff --git a/src/components/common/CustomButtonGroup.jsx b/src/components/common/CustomButtonGroup.jsx index 8fa5f80e..57c9800e 100644 --- a/src/components/common/CustomButtonGroup.jsx +++ b/src/components/common/CustomButtonGroup.jsx @@ -65,7 +65,7 @@ const CustomButtonGroup = ({ variant = "contained", options = [], sx }) => { }; return ( - + diff --git a/src/components/common/CustomViewButton.jsx b/src/components/common/CustomViewButton.jsx index 5b8fa5f1..95234b74 100644 --- a/src/components/common/CustomViewButton.jsx +++ b/src/components/common/CustomViewButton.jsx @@ -1,28 +1,54 @@ import PropTypes from 'prop-types'; -import {Button} from "@mui/material"; +import { useState } from 'react'; +import { Button } from "@mui/material"; +import FeatureNotAvailableDialog from "./FeatureNotAvailableDialog"; -import {vars} from "../../theme/variables"; +import { vars } from "../../theme/variables"; const { gray50, gray300 } = vars; -const CustomViewButton = ({ view, listView, onClick, icon }) => ( - -); +const CustomViewButton = ({ view, listView, onClick, icon }) => { + const [featureNotAvailableOpen, setFeatureNotAvailableOpen] = useState(false); + + const handleClick = () => { + if (view === 'table') { + setFeatureNotAvailableOpen(true); + } else { + onClick(); + } + }; + + const handleCloseFeatureDialog = () => { + setFeatureNotAvailableOpen(false); + }; + + return ( + <> + + + + + ); +}; CustomViewButton.propTypes = { view: PropTypes.string.isRequired, diff --git a/src/components/common/FeatureNotAvailableDialog.jsx b/src/components/common/FeatureNotAvailableDialog.jsx new file mode 100644 index 00000000..ae3501f1 --- /dev/null +++ b/src/components/common/FeatureNotAvailableDialog.jsx @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + Box, + Typography +} from '@mui/material'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; + +const FeatureNotAvailableDialog = ({ open, onClose, title = "Feature Not Yet Available", message = "This feature is not yet implemented. Please check back in a future update." }) => { + const handleCreateIssue = () => { + const issueTitle = encodeURIComponent(`Feature Request: ${title}`); + const issueBody = encodeURIComponent(`**Feature Description:** +${message} + +**Additional Context:** +Please describe what you would like to see implemented and any specific requirements or use cases. + +**Priority:** +Please indicate the priority level (Low/Medium/High) and any business justification. +`); + + const githubUrl = `https://github.com/MetaCell/interlex/issues/new?title=${issueTitle}&body=${issueBody}&labels=enhancement`; + window.open(githubUrl, '_blank', 'noopener,noreferrer'); + }; + + return ( + + + {title} + + + + {message} + + + + + Have feedback about this feature? + + + We'd love to hear your thoughts and suggestions! Click below to create a GitHub issue where you can share your ideas, requirements, or use cases. + + + + + + + + + ); +}; + +FeatureNotAvailableDialog.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + title: PropTypes.string, + message: PropTypes.string +}; + +export default FeatureNotAvailableDialog; \ No newline at end of file diff --git a/src/components/common/FeatureNotAvailableDialog.md b/src/components/common/FeatureNotAvailableDialog.md new file mode 100644 index 00000000..1f0c163b --- /dev/null +++ b/src/components/common/FeatureNotAvailableDialog.md @@ -0,0 +1,76 @@ +# FeatureNotAvailableDialog Component + +A reusable React component for displaying a modal when users try to access features that are not yet implemented. + +## Features + +- **User-friendly messaging**: Shows a clear message about the unavailable feature +- **GitHub integration**: Provides a button to create a GitHub issue for feature requests +- **Customizable**: Allows custom titles and messages +- **Pre-filled issue template**: Automatically creates issue templates with relevant context + +## Usage + +```jsx +import FeatureNotAvailableDialog from '../common/FeatureNotAvailableDialog'; + +const MyComponent = () => { + const [dialogOpen, setDialogOpen] = useState(false); + + const handleFeatureClick = () => { + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + }; + + return ( + <> + + + + + ); +}; +``` + +## Props + +| Prop | Type | Default | Required | Description | +|------|------|---------|----------|-------------| +| `open` | boolean | - | Yes | Controls dialog visibility | +| `onClose` | function | - | Yes | Callback when dialog is closed | +| `title` | string | "Feature Not Yet Available" | No | Dialog title | +| `message` | string | "This feature is not yet implemented..." | No | Dialog message | + +## GitHub Integration + +When users click "Create GitHub Issue", the component: + +1. Opens a new tab to the MetaCell/interlex GitHub repository +2. Pre-fills the issue title with the feature name +3. Creates a template with sections for: + - Feature Description + - Additional Context + - Priority + +## Current Usage + +- **SingleTermView**: Used for unimplemented review and other features +- **Table View**: Used in search results and organization views when users try to access table view +- **CustomViewButton**: Integrated to show dialog instead of disabled button + +## Implementation Notes + +- Uses Material-UI components for consistent styling +- Opens GitHub links in new tabs with security attributes +- Provides clear call-to-action for user feedback +- Styled with a gray background box to draw attention to the feedback section \ No newline at end of file diff --git a/src/components/common/OrganizationsList.jsx b/src/components/common/OrganizationsList.jsx index 9725ece1..50318572 100644 --- a/src/components/common/OrganizationsList.jsx +++ b/src/components/common/OrganizationsList.jsx @@ -63,33 +63,54 @@ const OrganizationsList = ({organizations, viewJoinButton = true}) => { } }}> {organizations.length > 0 ? ( - organizations?.map((organization, index) => ( - navigate(`/organizations/${organization}`)}> - - {organization} - - } - /> - - { - viewJoinButton && - } - - - )) + organizations?.map((organization, index) => { + const orgName = typeof organization === 'string' ? organization : organization.name; + const userRole = typeof organization === 'object' ? organization.role : null; + + return ( + navigate(`/organizations/${orgName}`)}> + + {orgName} + {userRole && ( + + {userRole} + + )} + + } + /> + + { + viewJoinButton && + } + + + ); + }) ) : ( There are no organizations )} diff --git a/src/helpers/useOrganizations.ts b/src/helpers/useOrganizations.ts index cff089cd..c465b007 100644 --- a/src/helpers/useOrganizations.ts +++ b/src/helpers/useOrganizations.ts @@ -1,38 +1,61 @@ import { useState, useEffect } from 'react'; import { getOrganizations, createNewOrganization } from '../api/endpoints/apiService'; +interface OrganizationData { + role: string | null; + name: string; +} + export const useOrganizations = (groupname: string) => { const [listView, setListView] = useState('list'); - const [organizations, setOrganizations] = useState([]); + const [organizations, setOrganizations] = useState([]); const [loading, setLoading] = useState(false); - const [message, setMessage] = useState(); + const [message, setMessage] = useState(); const fetchOrganizations = async () => { setLoading(true); try { const response = await getOrganizations(groupname); - if (response.length > 0) { - // Replace "owner" with the actual username/groupname - const processedOrganizations = response[0].map((org: string) => - org === "owner" ? groupname : org - ); + + if (response && response.length > 0) { + // The response is an array of [role, organizationName] pairs + const processedOrganizations = response.map((orgData: any) => { + // Each orgData should be a [role, name] array + if (Array.isArray(orgData) && orgData.length >= 2) { + return { + role: orgData[0], // First element is the role + name: orgData[1] // Second element is the organization name + }; + } else { + console.warn('Unexpected organization data format:', orgData); + // Fallback for unexpected format + return { + role: null, + name: String(orgData) + }; + } + }); + setOrganizations(processedOrganizations); + } else { + setOrganizations([]); } } catch (err) { console.error('An unknown error occurred: ', err); + setOrganizations([]); } finally { setLoading(false); } }; - const createOrganization = async (event) => { + const createOrganization = async (event?: React.FormEvent) => { if (event) event.preventDefault(); setLoading(true); try { await createNewOrganization({ group: groupname, data: "a test" }); - } catch (err) { + } catch (err: any) { console.error('An unknown error occurred: ', err); if (err.response?.status === 501) { setMessage(err.response.data); @@ -53,7 +76,6 @@ export const useOrganizations = (groupname: string) => { organizations, loading, message, - open, createOrganization, fetchOrganizations }; diff --git a/src/utils.js b/src/utils.js index e30361fc..26a8e9fe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -20,10 +20,14 @@ export const getComparator = (order, orderBy) => { } else if (bValue === '') { return -1; } else { + // Convert to lowercase for case-insensitive comparison + const aLower = typeof aValue === 'string' ? aValue.toLowerCase() : aValue; + const bLower = typeof bValue === 'string' ? bValue.toLowerCase() : bValue; + if (order === 'desc') { - return bValue < aValue ? -1 : 1; + return bLower < aLower ? -1 : 1; } else { - return aValue < bValue ? -1 : 1; + return aLower < bLower ? -1 : 1; } } }; From fb803a4da422e01896b5b3de5aad22fd6bae8aef Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Tue, 23 Sep 2025 00:24:42 +0200 Subject: [PATCH 2/2] send patch to groupname rather than base --- .../EditBulkTerms/EditBulkTermsDialog.jsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx b/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx index b0373b65..4a134379 100644 --- a/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx +++ b/src/components/Dashboard/EditBulkTerms/EditBulkTermsDialog.jsx @@ -71,7 +71,7 @@ HeaderRightSideContent.propTypes = { }; const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) => { - const { activeOntology } = useContext(GlobalDataContext); + const { activeOntology, user } = useContext(GlobalDataContext); const [searchConditions, setSearchConditions] = useState([initialSearchConditions]); const [ontologyTerms, setOntologyTerms] = useState([]); const [ontologyAttributes, setOntologyAttributes] = useState([]); @@ -88,6 +88,28 @@ const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) = } }, [open, activeOntology, selectedOntology]); + // Helper function to replace "base" with user groupname in term data + const replaceBaseWithUserGroup = (obj, userGroupname) => { + if (!obj || !userGroupname) return obj; + + const replaceInValue = (value) => { + if (typeof value === 'string') { + return value.replace(/\bbase\b/g, userGroupname); + } else if (Array.isArray(value)) { + return value.map(replaceInValue); + } else if (value && typeof value === 'object') { + return replaceBaseWithUserGroup(value, userGroupname); + } + return value; + }; + + const result = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = replaceInValue(value); + } + return result; + }; + const performBatchUpdate = async (termsToUpdate = null) => { setIsUpdating(true); @@ -117,12 +139,13 @@ const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) = termId = termId.split('/').pop(); } - const group = 'base'; // Default group, can be made configurable + // Use user's groupname instead of 'base' + const group = user?.groupname || 'base'; - // Create the JSON-LD payload with the current term data - const jsonLdPayload = { + // Create the JSON-LD payload with the current term data, replacing base with user groupname + let jsonLdPayload = { "@context": term["@context"] || { - "@vocab": "http://uri.interlex.org/base/", + "@vocab": `http://uri.interlex.org/${group}/`, "owl": "http://www.w3.org/2002/07/owl#", "rdfs": "http://www.w3.org/2000/01/rdf-schema#" }, @@ -130,6 +153,11 @@ const EditBulkTermsDialog = ({ open, handleClose, activeStep, setActiveStep }) = ...term }; + // Replace any "base" references with user's groupname in the payload + if (user?.groupname && user.groupname !== 'base') { + jsonLdPayload = replaceBaseWithUserGroup(jsonLdPayload, user.groupname); + } + // Send PATCH request const response = await patchTerm(group, termId, jsonLdPayload);