diff --git a/rsconcept/frontend/src/components/Common/Checkbox.tsx b/rsconcept/frontend/src/components/Common/Checkbox.tsx index 7f97692f..c29fdef7 100644 --- a/rsconcept/frontend/src/components/Common/Checkbox.tsx +++ b/rsconcept/frontend/src/components/Common/Checkbox.tsx @@ -1,3 +1,5 @@ +import { useRef } from 'react'; + import Label from './Label'; export interface CheckboxProps { @@ -11,21 +13,34 @@ export interface CheckboxProps { onChange?: (event: React.ChangeEvent) => void } -// TODO: implement disabled={disabled} function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-full', value, onChange }: CheckboxProps) { + const inputRef = useRef(null); + + const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer'; + + function handleLabelClick(event: React.MouseEvent): void { + event.preventDefault(); + if (!disabled) { + inputRef.current?.click(); + } + } + return (
- - { label &&
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx index e9426b2b..6f42065c 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx @@ -13,56 +13,32 @@ import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models' import { getCstTypePrefix, getCstTypeShortcut, getTypeLabel, mapStatusInfo } from '../../utils/staticUI'; interface EditorItemsProps { - onOpenEdit: (cst: IConstituenta) => void - onShowCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void + onOpenEdit: (cstID: number) => void + onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void + onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void } -function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { - const { - schema, isEditable, - cstDelete, cstMoveTo, resetAliases - } = useRSForm(); +function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) { + const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm(); const { noNavigation } = useConceptTheme(); const [selected, setSelected] = useState([]); const nothingSelected = useMemo(() => selected.length === 0, [selected]); const [toggledClearRows, setToggledClearRows] = useState(false); - const handleRowClicked = useCallback( - (cst: IConstituenta, event: React.MouseEvent) => { - if (event.altKey) { - onOpenEdit(cst); - } - }, [onOpenEdit]); - - const handleSelectionChange = useCallback( - ({ selectedRows }: { - allSelected: boolean - selectedCount: number - selectedRows: IConstituenta[] - }) => { - setSelected(selectedRows.map(cst => cst.id)); - }, [setSelected]); - // Delete selected constituents - const handleDelete = useCallback(() => { - if (!schema?.items || !window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) { + function handleDelete() { + if (!schema) { return; } - const data = { - items: selected.map(id => { return { id }; }) - } - const deletedNames = selected.map(id => schema.items?.find(cst => cst.id === id)?.alias).join(', '); - cstDelete(data, () => { - toast.success(`Конституенты удалены: ${deletedNames}`); + onDeleteCst(selected, () => { setToggledClearRows(prev => !prev); setSelected([]); }); - }, [selected, schema?.items, cstDelete]); + } // Move selected cst up - const handleMoveUp = useCallback( - () => { + function handleMoveUp() { if (!schema?.items || selected.length === 0) { return; } @@ -80,11 +56,10 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { move_to: target } cstMoveTo(data); - }, [selected, schema?.items, cstMoveTo]); + } // Move selected cst down - const handleMoveDown = useCallback( - () => { + function handleMoveDown() { if (!schema?.items || selected.length === 0) { return; } @@ -106,26 +81,25 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { move_to: target } cstMoveTo(data); - }, [selected, schema?.items, cstMoveTo]); + } // Generate new names for all constituents - const handleReindex = useCallback(() => { + function handleReindex() { resetAliases(() => toast.success('Переиндексация конституент успешна')); - }, [resetAliases]); + } - // Add new constituent - const handleAddNew = useCallback( - (type?: CstType) => { - if (!schema) { - return; - } - const selectedPosition = selected.reduce((prev, cstID) => { - const position = schema.items.findIndex(cst => cst.id === cstID); - return Math.max(position, prev); - }, -1); - const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined; - onShowCreateCst(insert_where, type, type !== undefined); - }, [schema, onShowCreateCst, selected]); + // Add new constituenta + function handleCreateCst(type?: CstType){ + if (!schema) { + return; + } + const selectedPosition = selected.reduce((prev, cstID) => { + const position = schema.items.findIndex(cst => cst.id === cstID); + return Math.max(position, prev); + }, -1); + const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined; + onCreateCst(insert_where, type, type !== undefined); + } // Implement hotkeys for working with constituents table function handleTableKey(event: React.KeyboardEvent) { @@ -154,18 +128,34 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { } } switch (key) { - case '1': handleAddNew(CstType.BASE); return true; - case '2': handleAddNew(CstType.STRUCTURED); return true; - case '3': handleAddNew(CstType.TERM); return true; - case '4': handleAddNew(CstType.AXIOM); return true; - case 'q': handleAddNew(CstType.FUNCTION); return true; - case 'w': handleAddNew(CstType.PREDICATE); return true; - case '5': handleAddNew(CstType.CONSTANT); return true; - case '6': handleAddNew(CstType.THEOREM); return true; + case '1': handleCreateCst(CstType.BASE); return true; + case '2': handleCreateCst(CstType.STRUCTURED); return true; + case '3': handleCreateCst(CstType.TERM); return true; + case '4': handleCreateCst(CstType.AXIOM); return true; + case 'q': handleCreateCst(CstType.FUNCTION); return true; + case 'w': handleCreateCst(CstType.PREDICATE); return true; + case '5': handleCreateCst(CstType.CONSTANT); return true; + case '6': handleCreateCst(CstType.THEOREM); return true; } return false; } + const handleRowClicked = useCallback( + (cst: IConstituenta, event: React.MouseEvent) => { + if (event.altKey) { + onOpenEdit(cst.id); + } + }, [onOpenEdit]); + + const handleSelectionChange = useCallback( + ({ selectedRows }: { + allSelected: boolean + selectedCount: number + selectedRows: IConstituenta[] + }) => { + setSelected(selectedRows.map(cst => cst.id)); + }, [setSelected]); + const columns = useMemo(() => [ { @@ -300,7 +290,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { tooltip='Новая конституента' icon={} dense - onClick={() => { handleAddNew(); }} + onClick={() => handleCreateCst()} /> {(Object.values(CstType)).map( (typeStr) => { @@ -309,7 +299,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { text={`${getCstTypePrefix(type)}`} tooltip={getCstTypeShortcut(type)} dense - onClick={() => { handleAddNew(type); }} + onClick={() => handleCreateCst(type)} />; })}
@@ -359,7 +349,7 @@ function EditorItems({ onOpenEdit, onShowCreateCst }: EditorItemsProps) { selectableRows selectableRowsHighlight onSelectedRowsChange={handleSelectionChange} - onRowDoubleClicked={onOpenEdit} + onRowDoubleClicked={cst => onOpenEdit(cst.id)} onRowClicked={handleRowClicked} clearSelectedRows={toggledClearRows} dense diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx index e616346e..54f1600c 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx @@ -30,7 +30,7 @@ function EditorTermGraph() { const result: GraphEdge[] = []; let edgeID = 1; schema?.graph.nodes.forEach(source => { - source.adjacent.forEach(target => { + source.outputs.forEach(target => { result.push({ id: String(edgeID), source: String(source.id), diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index 8ce9dd18..b1f66100 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useCallback, useLayoutEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import { TabList, TabPanel, Tabs } from 'react-tabs'; import { toast } from 'react-toastify'; @@ -7,12 +7,12 @@ import BackendError from '../../components/BackendError'; import ConceptTab from '../../components/Common/ConceptTab'; import { Loader } from '../../components/Common/Loader'; import { useRSForm } from '../../context/RSFormContext'; -import useLocalStorage from '../../hooks/useLocalStorage'; -import { prefixes, timeout_updateUI } from '../../utils/constants'; -import { CstType,type IConstituenta, ICstCreateData, SyntaxTree } from '../../utils/models'; +import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; +import { CstType, ICstCreateData, SyntaxTree } from '../../utils/models'; import { createAliasFor } from '../../utils/staticUI'; import DlgCloneRSForm from './DlgCloneRSForm'; import DlgCreateCst from './DlgCreateCst'; +import DlgDeleteCst from './DlgDeleteCst'; import DlgShowAST from './DlgShowAST'; import DlgUploadRSForm from './DlgUploadRSForm'; import EditorConstituenta from './EditorConstituenta'; @@ -31,73 +31,63 @@ export enum RSTabsList { function RSTabs() { const navigate = useNavigate(); - const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm(); + const search = useLocation().search; + const { + error, schema, loading, + cstCreate, cstDelete + } = useRSForm(); - const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD); - - const [init, setInit] = useState(false); + const [activeTab, setActiveTab] = useState(RSTabsList.CARD); + const [activeID, setActiveID] = useState(undefined) const [showUpload, setShowUpload] = useState(false); const [showClone, setShowClone] = useState(false); + const [syntaxTree, setSyntaxTree] = useState([]); const [expression, setExpression] = useState(''); const [showAST, setShowAST] = useState(false); + const [afterDelete, setAfterDelete] = useState<((items: number[]) => void) | undefined>(undefined); + const [toBeDeleted, setToBeDeleted] = useState([]); + const [showDeleteCst, setShowDeleteCst] = useState(false); + const [defaultType, setDefaultType] = useState(undefined); const [insertWhere, setInsertWhere] = useState(undefined); const [showCreateCst, setShowCreateCst] = useState(false); useLayoutEffect(() => { if (schema) { - const url = new URL(window.location.href); - const activeQuery = url.searchParams.get('active'); - const activeCst = schema.items.find((cst) => cst.id === Number(activeQuery)); - setActiveID(activeCst?.id); - setInit(true); - const oldTitle = document.title document.title = schema.title return () => { document.title = oldTitle } } - }, [setActiveID, schema, schema?.title, setInit]); + }, [schema]); - useEffect(() => { - const url = new URL(window.location.href); - const tabQuery = url.searchParams.get('tab'); - setActiveTab(Number(tabQuery) || RSTabsList.CARD); - }, [setActiveTab]); - - useEffect(() => { - if (init) { - const url = new URL(window.location.href); - const currentActive = url.searchParams.get('active'); - const currentTab = url.searchParams.get('tab'); - const saveHistory = activeTab === RSTabsList.CST_EDIT && currentActive !== String(activeID); - if (currentTab !== String(activeTab)) { - url.searchParams.set('tab', String(activeTab)); - } - if (activeID) { - if (currentActive !== String(activeID)) { - url.searchParams.set('active', String(activeID)); - } - } else { - url.searchParams.delete('active'); - } - if (saveHistory) { - window.history.pushState(null, '', url.toString()); - } else { - window.history.replaceState(null, '', url.toString()); - } - } - }, [activeTab, activeID, init]); + useLayoutEffect(() => { + const activeTab = Number(new URLSearchParams(search).get('tab')) ?? RSTabsList.CARD; + const cstQuery = new URLSearchParams(search).get('active'); + setActiveTab(activeTab); + setActiveID(Number(cstQuery) ?? (schema && schema?.items.length > 0 && schema?.items[0])); + }, [search, setActiveTab, setActiveID, schema]); function onSelectTab(index: number) { - setActiveTab(index); + navigateTo(index, activeID); } - const handleAddNew = useCallback( + const navigateTo = useCallback( + (tab: RSTabsList, activeID?: number) => { + if (activeID) { + navigate(`/rsforms/${schema!.id}?tab=${tab}&active=${activeID}`, { + replace: tab === activeTab && tab !== RSTabsList.CST_EDIT + }); + } else { + navigate(`/rsforms/${schema!.id}?tab=${tab}`); + } + }, [navigate, schema, activeTab]); + + const handleCreateCst = useCallback( (type: CstType, selectedCst?: number) => { if (!schema?.items) { return; @@ -109,32 +99,68 @@ function RSTabs() { } cstCreate(data, newCst => { toast.success(`Конституента добавлена: ${newCst.alias}`); - navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`); + navigateTo(activeTab, newCst.id); if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) { setTimeout(() => { const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`); if (element) { element.scrollIntoView({ behavior: 'smooth', - block: "end", - inline: "nearest" + block: 'end', + inline: 'nearest' }); } - }, timeout_updateUI); + }, TIMEOUT_UI_REFRESH); } }); - }, [schema, cstCreate, insertWhere, navigate, activeTab]); + }, [schema, cstCreate, insertWhere, navigateTo, activeTab]); - const onShowCreateCst = useCallback( - (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => { - if (skipDialog && type) { - handleAddNew(type, selectedID); - } else { - setDefaultType(type); - setInsertWhere(selectedID); - setShowCreateCst(true); + const promptCreateCst = useCallback( + (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => { + if (skipDialog && type) { + handleCreateCst(type, selectedID); + } else { + setDefaultType(type); + setInsertWhere(selectedID); + setShowCreateCst(true); + } + }, [handleCreateCst]); + + const handleDeleteCst = useCallback( + (deleted: number[]) => { + if (!schema) { + return; + } + const data = { + items: deleted.map(id => { return { id: id }; }) + }; + let activeIndex = schema.items.findIndex(cst => cst.id === activeID); + cstDelete(data, () => { + const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', '); + toast.success(`Конституенты удалены: ${deletedNames}`); + if (deleted.length === schema.items.length) { + navigateTo(RSTabsList.CST_LIST); } - }, [handleAddNew]); + if (activeIndex) { + while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) { + ++activeIndex; + } + navigateTo(activeTab, schema.items[activeIndex].id); + } + if (afterDelete) afterDelete(deleted); + }); + }, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTo]); + + + const promptDeleteCst = useCallback( + (selected: number[], callback?: (items: number[]) => void) => { + setAfterDelete(() => ( + (items: number[]) => { + if (callback) callback(items); + })); + setToBeDeleted(selected); + setShowDeleteCst(true) + }, []); const onShowAST = useCallback( (expression: string, ast: SyntaxTree) => { @@ -143,31 +169,41 @@ function RSTabs() { setShowAST(true); }, []); - const onEditCst = useCallback( - (cst: IConstituenta) => { - setActiveID(cst.id); - setActiveTab(RSTabsList.CST_EDIT) - }, [setActiveID, setActiveTab]); + const onOpenCst = useCallback( + (cstID: number) => { + navigateTo(RSTabsList.CST_EDIT, cstID) + }, [navigateTo]); return (
{ loading && } { error && } - { schema && !loading && - <> - {showUpload && { setShowUpload(false); }}/>} - {showClone && { setShowClone(false); }}/>} + { schema && !loading && <> + {showUpload && + setShowUpload(false)} + />} + {showClone && + setShowClone(false)} + />} {showAST && { setShowAST(false); }} + hideWindow={() => setShowAST(false)} />} {showCreateCst && { setShowCreateCst(false); }} - onCreate={handleAddNew} - defaultType={defaultType} + hideWindow={() => setShowCreateCst(false)} + onCreate={handleCreateCst} + defaultType={defaultType} + />} + {showDeleteCst && + setShowDeleteCst(false)} + onDelete={handleDeleteCst} + selected={toBeDeleted} />} - + - + - - } + }
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx index 45ada621..a83ea83f 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx @@ -7,16 +7,18 @@ import { useConceptTheme } from '../../../context/ThemeContext'; import useLocalStorage from '../../../hooks/useLocalStorage'; import { prefixes } from '../../../utils/constants'; import { CstType, extractGlobals,type IConstituenta, matchConstituenta } from '../../../utils/models'; -import { getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI'; +import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI'; import ConstituentaTooltip from './ConstituentaTooltip'; interface ViewSideConstituentsProps { expression: string + activeID?: number + onOpenEdit: (cstID: number) => void } -function ViewSideConstituents({ expression }: ViewSideConstituentsProps) { +function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) { const { darkMode } = useConceptTheme(); - const { schema, setActiveID, activeID } = useRSForm(); + const { schema } = useRSForm(); const [filteredData, setFilteredData] = useState(schema?.items ?? []); const [filterText, setFilterText] = useLocalStorage('side-filter-text', '') const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false); @@ -46,14 +48,16 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) { const handleRowClicked = useCallback( (cst: IConstituenta, event: React.MouseEvent) => { if (event.altKey && cst.id > 0) { - setActiveID(cst.id); + onOpenEdit(cst.id); } - }, [setActiveID]); + }, [onOpenEdit]); const handleDoubleClick = useCallback( (cst: IConstituenta) => { - if (cst.id > 0) setActiveID(cst.id); - }, [setActiveID]); + if (cst.id > 0) { + onOpenEdit(cst.id); + } + }, [onOpenEdit]); const conditionalRowStyles = useMemo(() => [ @@ -99,8 +103,7 @@ function ViewSideConstituents({ expression }: ViewSideConstituentsProps) { { name: 'Описание', id: 'description', - selector: (cst: IConstituenta) => - cst.term.resolved || cst.definition.text.resolved || cst.definition.formal || cst.convention, + selector: (cst: IConstituenta) => getCstDescription(cst), minWidth: '350px', wrap: true, conditionalCellStyles: [ diff --git a/rsconcept/frontend/src/utils/Graph.test.ts b/rsconcept/frontend/src/utils/Graph.test.ts index 85f4a36f..6f69b67d 100644 --- a/rsconcept/frontend/src/utils/Graph.test.ts +++ b/rsconcept/frontend/src/utils/Graph.test.ts @@ -14,4 +14,33 @@ describe('Testing Graph constuction', () => { graph.addEdge(13, 38); expect([... graph.nodes.keys()]).toStrictEqual([13, 37, 38]); }); + + test('creating from array', () => { + const graph = new Graph([[1, 2], [3], [4, 1]]); + expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]); + expect([... graph.nodes.get(1)!.outputs]).toStrictEqual([2]); + }); +}); + +describe('Testing Graph queries', () => { + test('expand outputs', () => { + const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]); + expect(graph.expandOutputs([])).toStrictEqual([]); + expect(graph.expandOutputs([3])).toStrictEqual([]); + expect(graph.expandOutputs([7])).toStrictEqual([]); + expect(graph.expandOutputs([2, 5])).toStrictEqual([3, 6, 1]); + }); + + test('expand into unique array', () => { + const graph = new Graph([[1, 2], [1, 3], [2, 5], [3, 5]]); + expect(graph.expandOutputs([1])).toStrictEqual([2, 3 ,5]); + }); + + test('expand inputs', () => { + const graph = new Graph([[1, 2], [2, 3], [2, 5], [5, 6], [6, 1], [7]]); + expect(graph.expandInputs([])).toStrictEqual([]); + expect(graph.expandInputs([7])).toStrictEqual([]); + expect(graph.expandInputs([6])).toStrictEqual([5, 2, 1]); + }); + }); \ No newline at end of file diff --git a/rsconcept/frontend/src/utils/Graph.ts b/rsconcept/frontend/src/utils/Graph.ts index 8e13e6f2..19b00f04 100644 --- a/rsconcept/frontend/src/utils/Graph.ts +++ b/rsconcept/frontend/src/utils/Graph.ts @@ -1,30 +1,49 @@ -// Graph class with basic comparison. Does not work for objects +// ======== ID based fast Graph implementation ============= export class GraphNode { id: number; - adjacent: number[]; + outputs: number[]; + inputs: number[]; constructor(id: number) { this.id = id; - this.adjacent = []; + this.outputs = []; + this.inputs = []; } - addAdjacent(node: number): void { - this.adjacent.push(node); + addOutput(node: number): void { + this.outputs.push(node); } - removeAdjacent(target: number): number | null { - const index = this.adjacent.findIndex(node => node === target); - if (index > -1) { - return this.adjacent.splice(index, 1)[0]; - } - return null; + addInput(node: number): void { + this.inputs.push(node); + } + + removeInput(target: number): number | null { + const index = this.inputs.findIndex(node => node === target); + return index > -1 ? this.inputs.splice(index, 1)[0] : null; + } + + removeOutput(target: number): number | null { + const index = this.outputs.findIndex(node => node === target); + return index > -1 ? this.outputs.splice(index, 1)[0] : null; } } export class Graph { nodes: Map = new Map(); - constructor() {} + constructor(arr?: number[][]) { + if (!arr) { + return; + } + arr.forEach(edge => { + if (edge.length == 1) { + this.addNode(edge[0]); + } else { + this.addEdge(edge[0], edge[1]); + } + }); + } addNode(target: number): GraphNode { let node = this.nodes.get(target); @@ -40,8 +59,9 @@ export class Graph { if (!nodeToRemove) { return null; } - this.nodes.forEach((node) => { - node.removeAdjacent(nodeToRemove.id); + this.nodes.forEach(node => { + node.removeInput(nodeToRemove.id); + node.removeOutput(nodeToRemove.id); }); this.nodes.delete(target); return nodeToRemove; @@ -50,38 +70,99 @@ export class Graph { addEdge(source: number, destination: number): void { const sourceNode = this.addNode(source); const destinationNode = this.addNode(destination); - sourceNode.addAdjacent(destinationNode.id); + sourceNode.addOutput(destinationNode.id); + destinationNode.addInput(sourceNode.id); } removeEdge(source: number, destination: number): void { const sourceNode = this.nodes.get(source); const destinationNode = this.nodes.get(destination); if (sourceNode && destinationNode) { - sourceNode.removeAdjacent(destination); + sourceNode.removeOutput(destination); + destinationNode.removeInput(source); } } + expandOutputs(origin: number[]): number[] { + const result: number[] = []; + const marked = new Map(); + origin.forEach(id => marked.set(id, true)); + origin.forEach(id => { + const node = this.nodes.get(id); + if (node) { + node.outputs.forEach(child => { + if (!marked.get(child) && !result.find(id => id === child)) { + result.push(child); + } + }); + } + }); + let position = 0; + while (position < result.length) { + const node = this.nodes.get(result[position]); + if (node && !marked.get(node.id)) { + marked.set(node.id, true); + node.outputs.forEach(child => { + if (!marked.get(child) && !result.find(id => id === child)) { + result.push(child); + } + }); + } + position += 1; + } + return result; + } + + expandInputs(origin: number[]): number[] { + const result: number[] = []; + const marked = new Map(); + origin.forEach(id => marked.set(id, true)); + origin.forEach(id => { + const node = this.nodes.get(id); + if (node) { + node.inputs.forEach(child => { + if (!marked.get(child) && !result.find(id => id === child)) { + result.push(child); + } + }); + } + }); + let position = 0; + while (position < result.length) { + const node = this.nodes.get(result[position]); + if (node && !marked.get(node.id)) { + marked.set(node.id, true); + node.inputs.forEach(child => { + if (!marked.get(child) && !result.find(id => id === child)) { + result.push(child); + } + }); + } + position += 1; + } + return result; + } + visitDFS(visitor: (node: GraphNode) => void) { const visited: Map = new Map(); this.nodes.forEach(node => { if (!visited.has(node.id)) { - this.depthFirstSearchAux(node, visited, visitor); + this.depthFirstSearch(node, visited, visitor); } }); } - private depthFirstSearchAux(node: GraphNode, visited: Map, visitor: (node: GraphNode) => void): void { - if (!node) { - return; - } + private depthFirstSearch( + node: GraphNode, + visited: Map, + visitor: (node: GraphNode) => void) + : void { visited.set(node.id, true); - visitor(node); - - node.adjacent.forEach((item) => { + node.outputs.forEach((item) => { if (!visited.has(item)) { - const childNode = this.nodes.get(item); - if (childNode) this.depthFirstSearchAux(childNode, visited, visitor); + const childNode = this.nodes.get(item)!; + this.depthFirstSearch(childNode, visited, visitor); } }); } diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index c5c2750b..21d47cb3 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -8,7 +8,7 @@ const dev = { }; export const config = process.env.NODE_ENV === 'production' ? prod : dev; -export const timeout_updateUI = 100; +export const TIMEOUT_UI_REFRESH = 100; export const urls = { concept: 'https://www.acconcept.ru/', diff --git a/rsconcept/frontend/src/utils/staticUI.ts b/rsconcept/frontend/src/utils/staticUI.ts index e7d7add9..4cb812e5 100644 --- a/rsconcept/frontend/src/utils/staticUI.ts +++ b/rsconcept/frontend/src/utils/staticUI.ts @@ -14,7 +14,7 @@ export interface IStatusInfo { tooltip: string } -export function getTypeLabel(cst: IConstituenta) { +export function getTypeLabel(cst: IConstituenta): string { if (cst.parse?.typification) { return cst.parse.typification; } @@ -24,6 +24,28 @@ export function getTypeLabel(cst: IConstituenta) { return 'Логический'; } +export function getCstDescription(cst: IConstituenta): string { + if (cst.cstType === CstType.STRUCTURED) { + return ( + cst.term.resolved || cst.term.raw || + cst.definition.text.resolved || cst.definition.text.raw || + cst.convention || + cst.definition.formal + ); + } else { + return ( + cst.term.resolved || cst.term.raw || + cst.definition.text.resolved || cst.definition.text.raw || + cst.definition.formal || + cst.convention + ); + } +} + +export function getCstLabel(cst: IConstituenta) { + return `${cst.alias}: ${getCstDescription(cst)}`; +} + export function getCstTypePrefix(type: CstType) { switch (type) { case CstType.BASE: return 'X';