diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 875dac86..2d519cd1 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -76,10 +76,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { const isEditable = useMemo( () => { return ( - !loading && !isReadonly && + !loading && !processing && !isReadonly && ((isOwned || (isForceAdmin && user?.is_staff)) ?? false) ) - }, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading]) + }, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading, processing]) const isTracking = useMemo( () => { diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx index 19fc76ce..3b25e045 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx @@ -90,7 +90,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) } // Add new constituenta - function handleCreateCst(type?: CstType){ + function handleCreateCst(type?: CstType) { if (!schema) { return; } diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx index dc64f2a7..ca893204 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph'; @@ -12,7 +12,7 @@ import MiniButton from '../../components/Common/MiniButton'; import InfoConstituenta from '../../components/Help/InfoConstituenta'; import InfoCstClass from '../../components/Help/InfoCstClass'; import CstStatusInfo from '../../components/Help/InfoCstStatus'; -import { ArrowsRotateIcon, FilterCogIcon, HelpIcon } from '../../components/Icons'; +import { ArrowsRotateIcon, DumpBinIcon, FilterCogIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons'; import { useRSForm } from '../../context/RSFormContext'; import { useConceptTheme } from '../../context/ThemeContext'; import useLocalStorage from '../../hooks/useLocalStorage'; @@ -57,12 +57,12 @@ export interface GraphEditorParams { interface EditorTermGraphProps { onOpenEdit: (cstID: number) => void - // onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void - // onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void + onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void + onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void } -function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { - const { schema } = useRSForm(); +function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) { + const { schema, isEditable } = useRSForm(); const { darkMode, noNavigation } = useConceptTheme(); const [ layout, setLayout ] = useLocalStorage('graph_layout', 'treeTd2d'); @@ -87,6 +87,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { const [ selectedDismissed, setSelectedDismissed ] = useState([]); const graphRef = useRef(null); const [showOptions, setShowOptions] = useState(false); + const [toggleUpdate, setToggleUpdate] = useState(false); const [hoverID, setHoverID] = useState(undefined); const hoverCst = useMemo( @@ -109,7 +110,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { return result; }, [allowBase, allowStruct, allowTerm, allowAxiom, allowFunction, allowPredicate, allowConstant, allowTheorem]); - useEffect( + useLayoutEffect( () => { if (!schema) { setFiltered(new Graph()); @@ -146,7 +147,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { setDismissed(newDismissed); setSelectedDismissed([]); setHoverID(undefined); - }, [schema, noHermits, noTransitive, noTemplates, allowedTypes]); + }, [schema, noHermits, noTransitive, noTemplates, allowedTypes, toggleUpdate]); function toggleDismissed(cstID: number) { setSelectedDismissed(prev => { @@ -199,6 +200,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { const { selections, actives, onNodeClick, + clearSelections, onCanvasClick, onNodePointerOver, onNodePointerOut @@ -213,9 +215,10 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { }); const allSelected: string[] = useMemo( - () => { - return [ ... selectedDismissed.map(id => String(id)), ... selections]; - }, [selectedDismissed, selections]); + () => { + return [ ... selectedDismissed.map(id => String(id)), ... selections]; + }, [selectedDismissed, selections]); + const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]); const handleRecreate = useCallback( () => { @@ -250,6 +253,43 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { if (onCanvasClick) onCanvasClick(event); }, [onCanvasClick]); + // Implement hotkeys for editing + function handleKeyDown(event: React.KeyboardEvent) { + console.log(event); + if (!isEditable) { + return; + } + if (event.key === 'Delete' && allSelected.length > 0) { + event.preventDefault(); + handleDeleteCst(); + return; + } + } + + function handleCreateCst() { + if (!schema) { + return; + } + const selectedPosition = allSelected.reduce((prev, cstID) => { + const position = schema.items.findIndex(cst => cst.id === Number(cstID)); + return Math.max(position, prev); + }, -1); + const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined; + onCreateCst(insert_where, undefined); + } + + function handleDeleteCst() { + if (!schema) { + return; + } + onDeleteCst([... allSelected.map(id => Number(id))], () => { + clearSelections(); + setDismissed([]); + setSelectedDismissed([]); + setToggleUpdate(prev => !prev); + }); + } + function getOptions() { return { noHermits: noHermits, @@ -305,7 +345,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { }, [selectedDismissed]); return ( -
+
{showOptions && setShowOptions(false)} @@ -321,11 +361,27 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { />
} -
- Выбраны - - {allSelected.length} из {schema?.stats?.count_all ?? 0} - +
+
+ Выбраны + + {allSelected.length} из {schema?.stats?.count_all ?? 0} + +
+
+ } + disabled={!isEditable || nothingSelected} + onClick={handleDeleteCst} + /> + } + disabled={!isEditable} + onClick={handleCreateCst} + /> +
@@ -401,7 +457,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
-
+