diff --git a/src/features/MergeNetworks/components/MatchingColumnTable.tsx b/src/features/MergeNetworks/components/MatchingColumnTable.tsx index f696898d..d5bef781 100644 --- a/src/features/MergeNetworks/components/MatchingColumnTable.tsx +++ b/src/features/MergeNetworks/components/MatchingColumnTable.tsx @@ -1,64 +1,100 @@ -import React from "react"; -import { Column } from "../../../models/TableModel/Column"; -import { NetworkRecord, Pair } from "../models/DataInterfaceForMerge"; -import { MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material" -import useMatchingColumnsStore from "../store/matchingColumnStore"; -import useNodeMatchingTableStore from "../store/nodeMatchingTableStore"; +import React from 'react' +import { Column } from '../../../models/TableModel/Column' +import { NetworkRecord, Pair } from '../models/DataInterfaceForMerge' +import { + MenuItem, + Paper, + Select, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material' +import useMatchingColumnsStore from '../store/matchingColumnStore' +import useNodeMatchingTableStore from '../store/nodeMatchingTableStore' +import useNodesDuplicationStore from '../store/nodesDuplicationStore' +import { checkDuplication } from '../utils/helper-functions' interface MatchingTableProps { - networkRecords: Record; - toMergeNetworksList: Pair[]; - matchingCols: Record; + networkRecords: Record + toMergeNetworksList: Pair[] + matchingCols: Record } -export const MatchingColumnTable = React.memo(({ networkRecords, toMergeNetworksList, matchingCols }: MatchingTableProps) => { - const placeHolderForMatchingCol = "Please select networks to merge..." - const setMatchingCols = useMatchingColumnsStore(state => state.setMatchingCols); - const updateNodeMatchingTable = useNodeMatchingTableStore(state => state.updateRow); +export const MatchingColumnTable = React.memo( + ({ + networkRecords, + toMergeNetworksList, + matchingCols, + }: MatchingTableProps) => { + const placeHolderForMatchingCol = 'Please select networks to merge...' + const setMatchingCols = useMatchingColumnsStore( + (state) => state.setMatchingCols, + ) + const updateNodeMatchingTable = useNodeMatchingTableStore( + (state) => state.updateRow, + ) + const setHasDuplication = useNodesDuplicationStore( + (state) => state.setHasDuplication, + ) // Handler for the 'Matching Columns' dropdown changes - const handleSetMatchingCols = (networkId: string) => (event: React.ChangeEvent) => { - const colType = networkRecords[networkId]?.nodeTable?.columns.find(col => col.name === event.target.value)?.type || 'None'; - if (colType === 'None') return; - const newCol: Column = { name: event.target.value, type: colType }; - setMatchingCols({ [networkId]: newCol }); - updateNodeMatchingTable(0, networkId, newCol); - }; + const handleSetMatchingCols = + (networkId: string) => (event: React.ChangeEvent) => { + const colType = + networkRecords[networkId]?.nodeTable?.columns.find( + (col) => col.name === event.target.value, + )?.type || 'None' + if (colType === 'None') return + const newCol: Column = { name: event.target.value, type: colType } + setMatchingCols({ [networkId]: newCol }) + updateNodeMatchingTable(0, networkId, newCol) + setHasDuplication( + networkId, + checkDuplication( + networkRecords[networkId]?.nodeTable, + event.target.value, + ), + ) + } return ( - - - - {toMergeNetworksList.length > 0 ? - ( - {toMergeNetworksList.map(net => ( - {net[0]} - ))} - ) : - ( - {placeHolderForMatchingCol} - ) - } - - - - {toMergeNetworksList.map(net => ( - - - - ))} - - -
-
+ + + + {toMergeNetworksList.length > 0 ? ( + + {toMergeNetworksList.map((net) => ( + {net[0]} + ))} + + ) : ( + + {placeHolderForMatchingCol} + + )} + + + + {toMergeNetworksList.map((net) => ( + + + + ))} + + +
+
) -} -) \ No newline at end of file + }, +) diff --git a/src/features/MergeNetworks/components/MatchingTableComp.tsx b/src/features/MergeNetworks/components/MatchingTableComp.tsx index d998d126..26496c2a 100644 --- a/src/features/MergeNetworks/components/MatchingTableComp.tsx +++ b/src/features/MergeNetworks/components/MatchingTableComp.tsx @@ -1,159 +1,296 @@ //import the necessary libraries and components -import React, { useEffect } from 'react'; -import { PriorityHigh as PriorityHighIcon } from '@mui/icons-material'; -import { MergeType, NetworkRecord, Pair, TableView } from '../models/DataInterfaceForMerge'; -import { TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, TextField, Tooltip } from '@mui/material'; -import { MatchingTableRow } from '../models/MatchingTable'; -import { NetAttDropDownTemplate } from './NetAttDropDownTemplate'; -import { TypeDropDownTemplate } from './TypeDropDownTemplate'; -import { IdType } from '../../../models/IdType'; -import { Column } from '../../../models/TableModel/Column'; -import useNodeMatchingTableStore from '../store/nodeMatchingTableStore'; -import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore'; -import useNetMatchingTableStore from '../store/netMatchingTableStore'; -import useMergeToolTipStore from '../store/mergeToolTip'; -import { ValueTypeName } from '../../../models/TableModel'; +import React, { useEffect } from 'react' +import { PriorityHigh as PriorityHighIcon } from '@mui/icons-material' +import { + MergeType, + NetworkRecord, + Pair, + TableView, +} from '../models/DataInterfaceForMerge' +import { + TableContainer, + Paper, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + TextField, + Tooltip, +} from '@mui/material' +import { MatchingTableRow } from '../models/MatchingTable' +import { NetAttDropDownTemplate } from './NetAttDropDownTemplate' +import { TypeDropDownTemplate } from './TypeDropDownTemplate' +import { IdType } from '../../../models/IdType' +import { Column } from '../../../models/TableModel/Column' +import useNodeMatchingTableStore from '../store/nodeMatchingTableStore' +import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore' +import useNetMatchingTableStore from '../store/netMatchingTableStore' +import useMergeToolTipStore from '../store/mergeToolTip' +import { ValueTypeName } from '../../../models/TableModel' interface MatchingTableProps { - networkRecords: Record - netLst: Pair[]; - tableView: TableView; - mergeOpType: MergeType; + networkRecords: Record + netLst: Pair[] + tableView: TableView + mergeOpType: MergeType + mergeWithinNetwork: boolean } -export const MatchingTableComp = React.memo(({ networkRecords, netLst, tableView, mergeOpType }: MatchingTableProps) => { - const tableData = (tableView === TableView.node) ? useNodeMatchingTableStore(state => state.rows) : - (tableView === TableView.edge ? useEdgeMatchingTableStore(state => state.rows) : useNetMatchingTableStore(state => state.rows)) - const setMatchingTable = (tableView === TableView.node) ? useNodeMatchingTableStore(state => state.setRow) : - (tableView === TableView.edge ? useEdgeMatchingTableStore(state => state.setRow) : useNetMatchingTableStore(state => state.setRow)); +export const MatchingTableComp = React.memo( + ({ + networkRecords, + netLst, + tableView, + mergeOpType, + mergeWithinNetwork, + }: MatchingTableProps) => { + const tableData = + tableView === TableView.node + ? useNodeMatchingTableStore((state) => state.rows) + : tableView === TableView.edge + ? useEdgeMatchingTableStore((state) => state.rows) + : useNetMatchingTableStore((state) => state.rows) + const setMatchingTable = + tableView === TableView.node + ? useNodeMatchingTableStore((state) => state.setRow) + : tableView === TableView.edge + ? useEdgeMatchingTableStore((state) => state.setRow) + : useNetMatchingTableStore((state) => state.setRow) // Handler for 'Merged Network' changes - const onMergedNetworkChange = (e: React.ChangeEvent, rowIndex: number, rowData: MatchingTableRow) => { - const updatedRow = { ...rowData, mergedNetwork: e.target.value }; - setMatchingTable(rowIndex, updatedRow); - }; - const setMergeTooltipIsOpen = useMergeToolTipStore(state => state.setIsOpen) - const setMergeTooltipText = useMergeToolTipStore(state => state.setText) - const name2RowId = new Map(); - const emptyRowIds = new Set(); + const onMergedNetworkChange = ( + e: React.ChangeEvent, + rowIndex: number, + rowData: MatchingTableRow, + ) => { + const updatedRow = { ...rowData, mergedNetwork: e.target.value } + setMatchingTable(rowIndex, updatedRow) + } + const setMergeTooltipIsOpen = useMergeToolTipStore( + (state) => state.setIsOpen, + ) + const setMergeTooltipText = useMergeToolTipStore((state) => state.setText) + const name2RowId = new Map() + const emptyRowIds = new Set() tableData.forEach((row) => { - const name = row.mergedNetwork; - if (name.length > 0) { - if (name2RowId.has(row.mergedNetwork)) { - name2RowId.get(row.mergedNetwork)?.push(row.id); - } else { - name2RowId.set(row.mergedNetwork, [row.id]); - } + const name = row.mergedNetwork + if (name.length > 0) { + if (name2RowId.has(row.mergedNetwork)) { + name2RowId.get(row.mergedNetwork)?.push(row.id) } else { - emptyRowIds.add(row.id); + name2RowId.set(row.mergedNetwork, [row.id]) } - }); + } else { + emptyRowIds.add(row.id) + } + }) //get rows that have duplicated name - const duplicatedNamesIds = new Set((Array.from(name2RowId.values()).filter((ids) => ids.length > 1)).reduce((acc, val) => acc.concat(val), [])); + const duplicatedNamesIds = new Set( + Array.from(name2RowId.values()) + .filter((ids) => ids.length > 1) + .reduce((acc, val) => acc.concat(val), []), + ) useEffect(() => { - if (netLst.length > 0) { - let isReady = true - if (duplicatedNamesIds.size > 0) { - isReady = false - setMergeTooltipText("Merge is disabled because there are duplicated network attribute names. Please ensure each attribute name is unique."); - } else if (emptyRowIds.size > 0) { - isReady = false - setMergeTooltipText("Merge is disabled because some network attribute names are empty. Please provide a name for each attribute."); - } - if (mergeOpType === MergeType.intersection && netLst.length < 2) { - isReady = false - setMergeTooltipText("Merge is disabled because intersection merge operation must take two or more networks. Please select at least two networks in the \'Networks to Merge\' list.") - } else if (mergeOpType === MergeType.difference && netLst.length !== 2) { - isReady = false - setMergeTooltipText("Merge is disabled because difference merge operation must take exactly two networks. Plesae select exactly two networks in the \'Networks to Merge\' list.") - } - if (tableView === TableView.node && tableData.length < 1) { - isReady = false - setMergeTooltipText("Merge is disabled because the node column table must contain at least one row.") - } else if (tableView === TableView.network && tableData.length < 3) { - isReady = false - setMergeTooltipText("Merge is disabled because the network column table must contain at least three rows.") - } - setMergeTooltipIsOpen(!isReady); - } else { - setMergeTooltipIsOpen(true); - setMergeTooltipText("Please select networks to merge") + if (netLst.length > 0) { + let isReady = true + if (duplicatedNamesIds.size > 0) { + isReady = false + setMergeTooltipText( + 'Merge is disabled because there are duplicated network attribute names. Please ensure each attribute name is unique.', + ) + } else if (emptyRowIds.size > 0) { + isReady = false + setMergeTooltipText( + 'Merge is disabled because some network attribute names are empty. Please provide a name for each attribute.', + ) + } + if ( + mergeOpType === MergeType.union && + netLst.length < 2 && + !mergeWithinNetwork + ) { + isReady = false + setMergeTooltipText( + 'Merge is disabled because only one network is selected and merge elements within network is disabled. Please select at least two networks in the "Networks to Merge" list or enable merge elements within network.', + ) + } else if ( + mergeOpType === MergeType.intersection && + netLst.length < 2 + ) { + isReady = false + setMergeTooltipText( + "Merge is disabled because intersection merge operation must take two or more networks. Please select at least two networks in the 'Networks to Merge' list.", + ) + } else if ( + mergeOpType === MergeType.difference && + netLst.length !== 2 + ) { + isReady = false + setMergeTooltipText( + "Merge is disabled because difference merge operation must take exactly two networks. Plesae select exactly two networks in the 'Networks to Merge' list.", + ) + } + if (tableView === TableView.node && tableData.length < 1) { + isReady = false + setMergeTooltipText( + 'Merge is disabled because the node column table must contain at least one row.', + ) + } else if (tableView === TableView.network && tableData.length < 3) { + isReady = false + setMergeTooltipText( + 'Merge is disabled because the network column table must contain at least three rows.', + ) } - }, [duplicatedNamesIds, emptyRowIds, netLst, mergeOpType]); + setMergeTooltipIsOpen(!isReady) + } else { + setMergeTooltipIsOpen(true) + setMergeTooltipText('Please select networks to merge') + } + }, [duplicatedNamesIds, emptyRowIds, netLst, mergeOpType]) - const getTooltipMessage = (row: MatchingTableRow, duplicatedNamesIds: Set, emptyRowIds: Set, netLst: Pair[]) => { - if (row.hasConflicts) { - const conflictDescription: string[] = []; - const typeSet = new Set(); - for (const [_, netId] of netLst) { - if (row.nameRecord[netId] !== 'None' && row.typeRecord[netId] !== undefined && !typeSet.has(row.typeRecord[netId])) { - conflictDescription.push(`${row.nameRecord[netId]}(${row.typeRecord[netId]})`); - typeSet.add(row.typeRecord[netId]); - } - } - const conflictStr: string[] = conflictDescription.length > 2 ? ['type conflicts', 'conflicts'] : ['a type conflict', 'conflict']; - return `This row has ${conflictStr[0]}: ${conflictDescription.join(', ')} . Please resolve the ${conflictStr[1]}.`; + const getTooltipMessage = ( + row: MatchingTableRow, + duplicatedNamesIds: Set, + emptyRowIds: Set, + netLst: Pair[], + ) => { + if (row.hasConflicts) { + const conflictDescription: string[] = [] + const typeSet = new Set() + for (const [_, netId] of netLst) { + if ( + row.nameRecord[netId] !== 'None' && + row.typeRecord[netId] !== undefined && + !typeSet.has(row.typeRecord[netId]) + ) { + conflictDescription.push( + `${row.nameRecord[netId]}(${row.typeRecord[netId]})`, + ) + typeSet.add(row.typeRecord[netId]) + } } - if (duplicatedNamesIds.has(row.id)) return "This row has a duplicated merged network attribute name. Please ensure each attribute name is unique."; - if (emptyRowIds.has(row.id)) return "This row has an empty merged network attribute name. Please provide a name for each attribute."; - return ''; - }; + const conflictStr: string[] = + conflictDescription.length > 2 + ? ['type conflicts', 'conflicts'] + : ['a type conflict', 'conflict'] + return `This row has ${conflictStr[0]}: ${conflictDescription.join(', ')} . Please resolve the ${conflictStr[1]}.` + } + if (duplicatedNamesIds.has(row.id)) + return 'This row has a duplicated merged network attribute name. Please ensure each attribute name is unique.' + if (emptyRowIds.has(row.id)) + return 'This row has an empty merged network attribute name. Please provide a name for each attribute.' + return '' + } return ( - - - - - {netLst.map((net) => ( - {net[0]} - ))} - Merged Network - Type - - - - {tableData.map((row, rowIndex) => ( - - - {netLst.map((net) => ( - - - - ))} - - {(row.id === 0 && tableView === TableView.node) ? - - onMergedNetworkChange(e, rowIndex, row)} - style={{ minWidth: 100 }} - InputProps={{ style: { color: 'red' } }} - /> - : - onMergedNetworkChange(e, rowIndex, row)} - style={{ minWidth: 100 }} - disabled={tableView === TableView.network && rowIndex < 3} - />} - - - - - - - ))} - -
-
+ + + + + {netLst.map((net) => ( + {net[0]} + ))} + Merged Network + Type + + + + {tableData.map((row, rowIndex) => ( + + + {netLst.map((net) => ( + + + + ))} + + {row.id === 0 && tableView === TableView.node ? ( + + + onMergedNetworkChange(e, rowIndex, row) + } + style={{ minWidth: 100 }} + InputProps={{ style: { color: 'red' } }} + /> + + ) : ( + + onMergedNetworkChange(e, rowIndex, row) + } + style={{ minWidth: 100 }} + disabled={ + tableView === TableView.network && rowIndex < 3 + } + /> + )} + + + + + + + ))} + +
+
) -}); \ No newline at end of file + }, +) diff --git a/src/features/MergeNetworks/components/MergeDialog.tsx b/src/features/MergeNetworks/components/MergeDialog.tsx index 659c360d..4d802209 100644 --- a/src/features/MergeNetworks/components/MergeDialog.tsx +++ b/src/features/MergeNetworks/components/MergeDialog.tsx @@ -9,8 +9,9 @@ import { Fullscreen as FullscreenIcon, FullscreenExit as FullscreenExitIcon, } from '@mui/icons-material' -import React, { ChangeEvent, useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react' import { UnionIcon, DifferenceIcon, IntersectionIcon } from './Icon' +import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred' import { Dialog, DialogTitle, @@ -69,6 +70,7 @@ import { LayoutAlgorithm, LayoutEngine } from '../../../models/LayoutModel' import { MatchingTableComp } from './MatchingTableComp' import { MatchingColumnTable } from './MatchingColumnTable' import { + checkDuplication, findPairIndex, getNetTableFromSummary, sortListAlphabetically, @@ -77,6 +79,7 @@ import { ConfirmationDialog } from '../../../components/Util/ConfirmationDialog' import { useNetworkSummaryStore } from '../../../store/NetworkSummaryStore' import { NdexNetworkSummary } from '../../../models/NetworkSummaryModel' import { useUiStateStore } from '../../../store/UiStateStore' +import useNodesDuplicationStore from '../store/nodesDuplicationStore' interface MergeDialogProps { open: boolean @@ -106,6 +109,18 @@ const MergeDialog: React.FC = ({ const [strictRemoveMode, setStrictRemoveMode] = useState(false) // Flag to indicate the rules of difference merge const [isNameDuplicate, setIsNameDuplicate] = useState(false) // Flag to indicate whether the network name is a duplicate const existingNetNames = new Set(workSpaceNetworks.map((pair) => pair[0])) // Set of existing network names + const nodesDuplication = useNodesDuplicationStore( + (state) => state.nodesDuplication, + ) + const setNodesDuplication = useNodesDuplicationStore( + (state) => state.setNodesDuplication, + ) + const removeNetInNodesDuplication = useNodesDuplicationStore( + (state) => state.removeNetworks, + ) + const resetNodesDuplication = useNodesDuplicationStore( + (state) => state.resetStore, + ) const mergeTooltipIsOpen = useMergeToolTipStore((state) => state.isOpen) const mergeTooltipText = useMergeToolTipStore((state) => state.text) // confirmation window @@ -258,6 +273,7 @@ const MergeDialog: React.FC = ({ } const newMatchingCols: Record = {} const newNetworkRecords: Record = {} + const newNodesDuplication: Record = {} for (const net of selectedAvailable) { // Load the network data let netData = networkRecords[net[1]] // Attempt to use cached data @@ -267,7 +283,8 @@ const MergeDialog: React.FC = ({ newNetworkRecords[net[1]] = netData // Set the default matching column for the network let hasName = false - for (const col of netData.nodeTable.columns ?? []) { + const nodeTable = netData.nodeTable + for (const col of nodeTable.columns ?? []) { if (col.name === 'name' && col.type === 'string') { newMatchingCols[net[1]] = { name: 'name', type: 'string' } as Column hasName = true @@ -276,11 +293,19 @@ const MergeDialog: React.FC = ({ } if (!hasName) { newMatchingCols[net[1]] = - netData.nodeTable.columns.length > 0 - ? netData.nodeTable.columns[0] + nodeTable.columns.length > 0 + ? nodeTable.columns[0] : ({ name: 'none', type: 'string' } as Column) } + if (nodeTable.columns.length > 0) { + const matchingColName = newMatchingCols[net[1]].name + newNodesDuplication[net[1]] = checkDuplication( + nodeTable, + matchingColName, + ) + } } + setNodesDuplication(newNodesDuplication) // Add the networks to the matching tables addNetsToNodeTable( selectedAvailable.map((pair) => pair[1]), @@ -326,6 +351,7 @@ const MergeDialog: React.FC = ({ removeNetsFromEdgeTable(selectedToMerge.map((pair) => pair[1])) removeNetsFromNetTable(selectedToMerge.map((pair) => pair[1])) // Update the state stores + removeNetInNodesDuplication(selectedToMerge.map((pair) => pair[1])) setNetworkRecords(newNetworkRecords) setMatchingCols(newMatchingCols) setSelectedToMerge([]) @@ -415,6 +441,7 @@ const MergeDialog: React.FC = ({ resetEdgeMatchingTable() resetNetMatchingTable() resetMatchingCols() + resetNodesDuplication() } }, []) // set merge type @@ -686,7 +713,7 @@ const MergeDialog: React.FC = ({ }} /> {isNameDuplicate && ( - + Warning: A network with this name already exists in your workspace. )} @@ -754,21 +781,33 @@ const MergeDialog: React.FC = ({ > - {network[0]} + + {network[0]} {index === 0 && ( )} - + {nodesDuplication[network[1]] && ( + + + + )} + } /> @@ -803,6 +842,53 @@ const MergeDialog: React.FC = ({ + {Object.values(nodesDuplication).some((v) => v === true) && ( + + + + Some nodes have duplicate values under the 'Matching Column'. + Hover over the warning icon or check 'Advanced Options' for + details. Enabling 'Merge nodes/edges in the same network' might + also be an option. + + + )} + + + setMergeWithinNetwork(e.target.checked)} + name="mergeWithinNetwork" + color="primary" + /> + } + label="Enable merging nodes/edges in the same network" + /> + + setMergeOnlyNodes(e.target.checked)} + name="mergeOnlyNodes" + color="primary" + disabled={MergeType.intersection !== mergeOpType} + /> + } + label="Merge only nodes and ignore edges" + /> + + + }> Advanced Options @@ -827,6 +913,7 @@ const MergeDialog: React.FC = ({ netLst={toMergeNetworksList} tableView={tableView} mergeOpType={mergeOpType} + mergeWithinNetwork={mergeWithinNetwork} /> )} @@ -867,42 +954,6 @@ const MergeDialog: React.FC = ({ - - setMergeWithinNetwork(e.target.checked)} - name="mergeWithinNetwork" - color="primary" - /> - } - label="Enable merging nodes/edges in the same network" - /> - - setMergeOnlyNodes(e.target.checked)} - name="mergeOnlyNodes" - color="primary" - disabled={MergeType.intersection !== mergeOpType} - /> - } - label="Merge only nodes and ignore edges" - /> - - diff --git a/src/features/MergeNetworks/components/NetAttDropDownTemplate.tsx b/src/features/MergeNetworks/components/NetAttDropDownTemplate.tsx index 0348aadc..0a3bec51 100644 --- a/src/features/MergeNetworks/components/NetAttDropDownTemplate.tsx +++ b/src/features/MergeNetworks/components/NetAttDropDownTemplate.tsx @@ -1,73 +1,124 @@ -import React from 'react'; -import { MatchingTableRow } from '../models/MatchingTable'; -import { NetworkRecord, TableView } from '../models/DataInterfaceForMerge'; -import { FormControl, MenuItem, Select, SelectChangeEvent } from '@mui/material'; -import { Column } from '../../../models/TableModel/Column'; -import { IdType } from '../../../models/IdType'; -import { ValueTypeName } from '../../../models/TableModel'; -import { getResonableCompatibleConvertionType } from '../utils/attributes-operations'; -import useMatchingColumnsStore from '../store/matchingColumnStore'; -import useNodeMatchingTableStore from '../store/nodeMatchingTableStore'; -import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore'; -import useNetMatchingTableStore from '../store/netMatchingTableStore'; +import React from 'react' +import { MatchingTableRow } from '../models/MatchingTable' +import { NetworkRecord, TableView } from '../models/DataInterfaceForMerge' +import { FormControl, MenuItem, Select, SelectChangeEvent } from '@mui/material' +import { Column } from '../../../models/TableModel/Column' +import { IdType } from '../../../models/IdType' +import { ValueTypeName } from '../../../models/TableModel' +import { getResonableCompatibleConvertionType } from '../utils/attributes-operations' +import useMatchingColumnsStore from '../store/matchingColumnStore' +import useNodeMatchingTableStore from '../store/nodeMatchingTableStore' +import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore' +import useNetMatchingTableStore from '../store/netMatchingTableStore' +import useNodesDuplicationStore from '../store/nodesDuplicationStore' +import { checkDuplication } from '../utils/helper-functions' interface netAttDropDownTemplateProps { - networkRecords: Record - rowData: MatchingTableRow; - rowIndex: number; - column: string; - type: TableView; - netLst: [string, string][]; + networkRecords: Record + rowData: MatchingTableRow + rowIndex: number + column: string + type: TableView + netLst: [string, string][] } - // Editable cell template for the network attributes -export const NetAttDropDownTemplate = React.memo(({ networkRecords, rowData, rowIndex, column, type, netLst }: netAttDropDownTemplateProps) => { - const emptyOption = { label: 'None', value: 'None' }; - const tableType = type === TableView.node ? 'nodeTable' : (type === TableView.edge ? 'edgeTable' : 'netTable'); - const columns = networkRecords[column]?.[tableType]?.columns || []; - const networkOptions = ((type === TableView.node && rowData.id === 0) || (type === TableView.network && rowData.id < 3)) ? columns.map(nc => ({ label: nc.name, value: nc.name })) : [...columns.map(nc => ({ label: nc.name, value: nc.name })), emptyOption]; - const currentValue = (rowData.nameRecord[column] && rowData.nameRecord[column] !== 'None') ? rowData.nameRecord[column] : ''; - const netIdLst = netLst.map(pair => pair[1]); - const setMatchingCols = useMatchingColumnsStore(state => state.setMatchingCols); - const setMatchingTable = (type === TableView.node) ? useNodeMatchingTableStore(state => state.setRow) : - (type === TableView.edge ? useEdgeMatchingTableStore(state => state.setRow) : useNetMatchingTableStore(state => state.setRow)); +export const NetAttDropDownTemplate = React.memo( + ({ + networkRecords, + rowData, + rowIndex, + column, + type, + netLst, + }: netAttDropDownTemplateProps) => { + const emptyOption = { label: 'None', value: 'None' } + const tableType = + type === TableView.node + ? 'nodeTable' + : type === TableView.edge + ? 'edgeTable' + : 'netTable' + const columns = networkRecords[column]?.[tableType]?.columns || [] + const networkOptions = + (type === TableView.node && rowData.id === 0) || + (type === TableView.network && rowData.id < 3) + ? columns.map((nc) => ({ label: nc.name, value: nc.name })) + : [ + ...columns.map((nc) => ({ label: nc.name, value: nc.name })), + emptyOption, + ] + const currentValue = + rowData.nameRecord[column] && rowData.nameRecord[column] !== 'None' + ? rowData.nameRecord[column] + : '' + const netIdLst = netLst.map((pair) => pair[1]) + const setMatchingCols = useMatchingColumnsStore( + (state) => state.setMatchingCols, + ) + const setHasDuplication = useNodesDuplicationStore( + (state) => state.setHasDuplication, + ) + const setMatchingTable = + type === TableView.node + ? useNodeMatchingTableStore((state) => state.setRow) + : type === TableView.edge + ? useEdgeMatchingTableStore((state) => state.setRow) + : useNetMatchingTableStore((state) => state.setRow) // Handler for 'Dropdown' changes - const onDropdownChange = (e: SelectChangeEvent, tableType: TableView, rowData: MatchingTableRow, field: string) => { - const newName = e.target.value; - const newType = columns.find(col => col.name === newName)?.type || 'None'; - const newNameRecord = { ...rowData.nameRecord, [field]: newName }; - const newTypeRecord: Record = { ...rowData.typeRecord, [field]: newType }; - const typeSet = new Set(); - Object.values(newTypeRecord).forEach(type => { - if (type !== 'None') typeSet.add(type as ValueTypeName) - }); - const hasConflicts = typeSet.size > 1; - const mergedType = getResonableCompatibleConvertionType(typeSet); - const updatedRow: MatchingTableRow = { - ...rowData, nameRecord: newNameRecord, typeRecord: newTypeRecord, hasConflicts, type: mergedType - }; - if (tableType === TableView.node && rowData.id === 0) { - setMatchingCols({ [field]: { name: newName, type: newType } as Column }); - } - setMatchingTable(rowIndex, updatedRow); - }; + const onDropdownChange = ( + e: SelectChangeEvent, + tableType: TableView, + rowData: MatchingTableRow, + field: string, + ) => { + const newName = e.target.value + const newType = + columns.find((col) => col.name === newName)?.type || 'None' + const newNameRecord = { ...rowData.nameRecord, [field]: newName } + const newTypeRecord: Record = { + ...rowData.typeRecord, + [field]: newType, + } + const typeSet = new Set() + Object.values(newTypeRecord).forEach((type) => { + if (type !== 'None') typeSet.add(type as ValueTypeName) + }) + const hasConflicts = typeSet.size > 1 + const mergedType = getResonableCompatibleConvertionType(typeSet) + const updatedRow: MatchingTableRow = { + ...rowData, + nameRecord: newNameRecord, + typeRecord: newTypeRecord, + hasConflicts, + type: mergedType, + } + if (tableType === TableView.node && rowData.id === 0) { + setMatchingCols({ [field]: { name: newName, type: newType } as Column }) + setHasDuplication( + field, + checkDuplication(networkRecords[field]?.nodeTable, newName), + ) + } + setMatchingTable(rowIndex, updatedRow) + } return ( - - - - ); -}); \ No newline at end of file + + + + ) + }, +) diff --git a/src/features/MergeNetworks/components/TypeDropDownTemplate.tsx b/src/features/MergeNetworks/components/TypeDropDownTemplate.tsx index 14741279..55b38493 100644 --- a/src/features/MergeNetworks/components/TypeDropDownTemplate.tsx +++ b/src/features/MergeNetworks/components/TypeDropDownTemplate.tsx @@ -1,45 +1,63 @@ -import React from "react"; -import { FormControl, MenuItem, Select, SelectChangeEvent } from "@mui/material"; -import { TableView } from "../models/DataInterfaceForMerge"; -import { getAllConvertiableTypes } from "../utils/attributes-operations"; -import { MatchingTableRow } from "../models/MatchingTable"; -import { ValueTypeName } from "../../../models/TableModel"; -import useNodeMatchingTableStore from '../store/nodeMatchingTableStore'; -import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore'; -import useNetMatchingTableStore from '../store/netMatchingTableStore'; +import React from 'react' +import { FormControl, MenuItem, Select, SelectChangeEvent } from '@mui/material' +import { TableView } from '../models/DataInterfaceForMerge' +import { getAllConvertiableTypes } from '../utils/attributes-operations' +import { MatchingTableRow } from '../models/MatchingTable' +import { ValueTypeName } from '../../../models/TableModel' +import useNodeMatchingTableStore from '../store/nodeMatchingTableStore' +import useEdgeMatchingTableStore from '../store/edgeMatchingTableStore' +import useNetMatchingTableStore from '../store/netMatchingTableStore' interface typeDropDownTemplateProps { - type: TableView; - rowData: MatchingTableRow; - rowIndex: number; - netLst: [string, string][]; + type: TableView + rowData: MatchingTableRow + rowIndex: number + netLst: [string, string][] } -export const TypeDropDownTemplate = React.memo(({ type, rowData, rowIndex, netLst }: typeDropDownTemplateProps) => { - const typeLst: Set = new Set(netLst.filter(pair => rowData.typeRecord.hasOwnProperty(pair[1])).map(pair => rowData.typeRecord[pair[1]])); - const typeOptions = getAllConvertiableTypes(typeLst).map(type => ({ label: type, value: type })); - const setMatchingTable = (type === TableView.node) ? useNodeMatchingTableStore(state => state.setRow) : - (type === TableView.edge ? useEdgeMatchingTableStore(state => state.setRow) : useNetMatchingTableStore(state => state.setRow)); - const onDropDownChange = (e: SelectChangeEvent, rowData: MatchingTableRow) => { - const updatedRow: MatchingTableRow = { ...rowData, type: e.target.value as ValueTypeName }; - setMatchingTable(rowIndex, updatedRow); +export const TypeDropDownTemplate = React.memo( + ({ type, rowData, rowIndex, netLst }: typeDropDownTemplateProps) => { + const typeLst: Set = new Set( + netLst + .filter((pair) => rowData.typeRecord.hasOwnProperty(pair[1])) + .map((pair) => rowData.typeRecord[pair[1]]), + ) + const typeOptions = getAllConvertiableTypes(typeLst).map((type) => ({ + label: type, + value: type, + })) + const setMatchingTable = + type === TableView.node + ? useNodeMatchingTableStore((state) => state.setRow) + : type === TableView.edge + ? useEdgeMatchingTableStore((state) => state.setRow) + : useNetMatchingTableStore((state) => state.setRow) + const onDropDownChange = ( + e: SelectChangeEvent, + rowData: MatchingTableRow, + ) => { + const updatedRow: MatchingTableRow = { + ...rowData, + type: e.target.value as ValueTypeName, + } + setMatchingTable(rowIndex, updatedRow) } return ( - - - + + + ) -}); - + }, +) diff --git a/src/features/MergeNetworks/models/Impl/CreateMergedNetworkWithView.ts b/src/features/MergeNetworks/models/Impl/CreateMergedNetworkWithView.ts index f4de0bfd..19b64904 100644 --- a/src/features/MergeNetworks/models/Impl/CreateMergedNetworkWithView.ts +++ b/src/features/MergeNetworks/models/Impl/CreateMergedNetworkWithView.ts @@ -118,7 +118,7 @@ export const createMergedNetworkWithView = async ( owner: '', version: mergedNetSummary.mergedVersion, completed: false, - visibility: Visibility.PUBLIC, + visibility: Visibility.LOCAL, nodeCount: newNetwork.nodes.length, edgeCount: newNetwork.edges.length, description: mergedNetSummary.mergedDescription, diff --git a/src/features/MergeNetworks/store/nodesDuplicationStore.ts b/src/features/MergeNetworks/store/nodesDuplicationStore.ts new file mode 100644 index 00000000..3fb6b197 --- /dev/null +++ b/src/features/MergeNetworks/store/nodesDuplicationStore.ts @@ -0,0 +1,47 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' + +interface NodesDuplicationState { + nodesDuplication: Record +} + +interface NodesDuplicationAction { + setHasDuplication: (netId: string, hasDuplication: boolean) => void + setNodesDuplication: (nodesDuplication: Record) => void + removeNetworks: (netId: string | string[]) => void + resetStore: () => void +} + +type NodesDuplicationStore = NodesDuplicationState & NodesDuplicationAction + +const useNodesDuplicationStore = create( + immer((set) => ({ + nodesDuplication: {}, + setHasDuplication: (netId: string, hasDuplication: boolean) => + set((state) => ({ + nodesDuplication: { + ...state.nodesDuplication, + [netId]: hasDuplication, + }, + })), + setNodesDuplication: (newNodesDuplication: Record) => + set((state) => ({ + nodesDuplication: { ...state.nodesDuplication, ...newNodesDuplication }, + })), + removeNetworks: (netIds: string | string[]) => + set((state) => { + if (typeof netIds === 'string') { + delete state.nodesDuplication[netIds] + } else { + netIds.forEach((netId) => delete state.nodesDuplication[netId]) + } + return state + }), + resetStore: () => + set(() => ({ + nodesDuplication: {}, + })), + })), +) + +export default useNodesDuplicationStore diff --git a/src/features/MergeNetworks/utils/helper-functions.ts b/src/features/MergeNetworks/utils/helper-functions.ts index e4cf7aa5..b6fb483d 100644 --- a/src/features/MergeNetworks/utils/helper-functions.ts +++ b/src/features/MergeNetworks/utils/helper-functions.ts @@ -88,4 +88,18 @@ export function sortListAlphabetically(list: [string, IdType][]): [string, IdTyp } return 0; }); -} \ No newline at end of file +} + +export const checkDuplication = (table: Table|undefined, columnName: string): boolean => { + if (!table) { + return false; + } + const values = Array.from(table.rows.values()).map(row => row[columnName]); + + const normalizedValues = values.map(value => JSON.stringify(value)); + + const uniqueValues = new Set(normalizedValues); + + return normalizedValues.length !== uniqueValues.size; + }; + \ No newline at end of file