diff --git a/TODO.txt b/TODO.txt index 640618f5..fe89ffeb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,7 +6,6 @@ For more specific TODOs see comments in code - home page - manuals - текстовый модуль для разрешения отсылок -- компонент для форматирования в редакторе текста (формальное выражения + отсылки в тексте) - блок нотификаций пользователей - блок синтеза - блок организации библиотеки моделей @@ -17,8 +16,10 @@ For more specific TODOs see comments in code - Use migtation/fixtures to provide initial data for testing - USe migtation/fixtures to load example common data +- create custom Select component +- reload react-data-table-component + [deployment] -- HTTPS - database backup daemon - logs collection - status dashboard for servers diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx index 6769c85d..a0abb79f 100644 --- a/rsconcept/frontend/src/App.tsx +++ b/rsconcept/frontend/src/App.tsx @@ -22,14 +22,14 @@ function App () { const scrollWindowSize = useMemo( () => { return !noNavigation ? - 'max-h-[calc(100vh-4.5rem)]' - : 'max-h-[100vh]'; + 'calc(100vh - 4.5rem)' + : '100vh'; }, [noNavigation]); const mainSize = useMemo( () => { return !noNavigation ? - 'min-h-[calc(100vh-12rem)]' - : 'min-h-[calc(100vh-8rem)] '; + 'calc(100vh - 12rem)' + : '100vh'; }, [noNavigation]); return ( @@ -42,8 +42,8 @@ function App () { pauseOnFocusLoss={false} /> -
-
+
+
} /> @@ -60,7 +60,7 @@ function App () { } />
-
); diff --git a/rsconcept/frontend/src/components/Common/Button.tsx b/rsconcept/frontend/src/components/Common/Button.tsx index 0e2f9524..c4458d47 100644 --- a/rsconcept/frontend/src/components/Common/Button.tsx +++ b/rsconcept/frontend/src/components/Common/Button.tsx @@ -27,7 +27,7 @@ function Button({ disabled={disabled ?? loading} onClick={onClick} title={tooltip} - className={`inline-flex items-center gap-2 align-middle justify-center ${padding} ${borderClass} ${colorClass} ${widthClass} ${cursor}`} + className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${borderClass} ${colorClass} ${widthClass} ${cursor}`} {...props} > {icon && {icon}} diff --git a/rsconcept/frontend/src/components/Common/SubmitButton.tsx b/rsconcept/frontend/src/components/Common/SubmitButton.tsx index 84ef8293..04b6b66f 100644 --- a/rsconcept/frontend/src/components/Common/SubmitButton.tsx +++ b/rsconcept/frontend/src/components/Common/SubmitButton.tsx @@ -8,7 +8,7 @@ interface SubmitButtonProps { function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: SubmitButtonProps) { return ( } {!noNavigation &&
-
+
- {user && } +
} description='Общие схемы' onClick={navigateCommon} /> } description='Справка' onClick={navigateHelp} /> diff --git a/rsconcept/frontend/src/components/Navigation/NavigationButton.tsx b/rsconcept/frontend/src/components/Navigation/NavigationButton.tsx index f043afbd..26d71a99 100644 --- a/rsconcept/frontend/src/components/Navigation/NavigationButton.tsx +++ b/rsconcept/frontend/src/components/Navigation/NavigationButton.tsx @@ -1,15 +1,17 @@ interface NavigationButtonProps { + id?: string icon: React.ReactNode - description: string + description?: string colorClass?: string - onClick: () => void + onClick?: () => void } const defaultColors = 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white' -function NavigationButton({ icon, description, colorClass = defaultColors, onClick }: NavigationButtonProps) { +function NavigationButton({ id, icon, description, colorClass = defaultColors, onClick }: NavigationButtonProps) { return ( -
); 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/DlgGraphOptions.tsx b/rsconcept/frontend/src/pages/RSFormPage/DlgGraphOptions.tsx new file mode 100644 index 00000000..e946e815 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/DlgGraphOptions.tsx @@ -0,0 +1,153 @@ +import { useLayoutEffect, useState } from 'react'; + +import Checkbox from '../../components/Common/Checkbox'; +import Modal from '../../components/Common/Modal'; +import { CstType } from '../../utils/models'; +import { getCstTypeLabel } from '../../utils/staticUI'; +import { GraphEditorParams } from './EditorTermGraph'; + +interface DlgGraphOptionsProps { + hideWindow: () => void + initial: GraphEditorParams + onConfirm: (params: GraphEditorParams) => void +} + +function DlgGraphOptions({ hideWindow, initial, onConfirm }:DlgGraphOptionsProps) { + const [ noHermits, setNoHermits ] = useState(true); + const [ noTransitive, setNoTransitive ] = useState(false); + const [ noTemplates, setNoTemplates ] = useState(true); + const [ noTerms, setNoTerms ] = useState(true); + + const [ allowBase, setAllowBase ] = useState(true); + const [ allowStruct, setAllowStruct ] = useState(true); + const [ allowTerm, setAllowTerm ] = useState(true); + const [ allowAxiom, setAllowAxiom ] = useState(true); + const [ allowFunction, setAllowFunction ] = useState(true); + const [ allowPredicate, setAllowPredicate ] = useState(true); + const [ allowConstant, setAllowConstant ] = useState(true); + const [ allowTheorem, setAllowTheorem ] = useState(true); + + function getParams() { + return { + noHermits: noHermits, + noTransitive: noTransitive, + noTemplates: noTemplates, + noTerms: noTerms, + + allowBase: allowBase, + allowStruct: allowStruct, + allowTerm: allowTerm, + allowAxiom: allowAxiom, + allowFunction: allowFunction, + allowPredicate: allowPredicate, + allowConstant: allowConstant, + allowTheorem: allowTheorem + } + } + + const handleSubmit = () => { + hideWindow(); + onConfirm(getParams()); + }; + + useLayoutEffect(() => { + setNoHermits(initial.noHermits); + setNoTransitive(initial.noTransitive); + setNoTemplates(initial.noTemplates); + setNoTerms(initial.noTerms); + + setAllowBase(initial.allowBase); + setAllowStruct(initial.allowStruct); + setAllowTerm(initial.allowTerm); + setAllowAxiom(initial.allowAxiom); + setAllowFunction(initial.allowFunction); + setAllowPredicate(initial.allowPredicate); + setAllowConstant(initial.allowConstant); + setAllowTheorem(initial.allowTheorem); + }, [initial]); + + return ( + +
+
+

Преобразования

+ setNoTerms(event.target.checked) } + /> + setNoHermits(event.target.checked) } + /> + setNoTemplates(event.target.checked) } + /> + setNoTransitive(event.target.checked) } + /> +
+
+

Типы конституент

+ setAllowBase(event.target.checked) } + /> + setAllowStruct(event.target.checked) } + /> + setAllowTerm(event.target.checked) } + /> + setAllowAxiom(event.target.checked) } + /> + setAllowFunction(event.target.checked) } + /> + setAllowPredicate(event.target.checked) } + /> + setAllowConstant(event.target.checked) } + /> + setAllowTheorem(event.target.checked) } + /> +
+
+
+ ); +} + +export default DlgGraphOptions; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx index de5bbe0b..b71658d5 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx @@ -6,10 +6,11 @@ import Divider from '../../components/Common/Divider'; import MiniButton from '../../components/Common/MiniButton'; import SubmitButton from '../../components/Common/SubmitButton'; import TextArea from '../../components/Common/TextArea'; +import CstStatusInfo from '../../components/Help/InfoCstStatus'; import { DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons'; import { useRSForm } from '../../context/RSFormContext'; import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/models'; -import { getCstTypeLabel, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI'; +import { getCstTypeLabel, getCstTypificationLabel } from '../../utils/staticUI'; import EditorRSExpression from './EditorRSExpression'; import ViewSideConstituents from './elements/ViewSideConstituents'; @@ -145,19 +146,19 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
- } - /> } /> -
+ } + /> +
@@ -173,18 +174,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe

- при наведении на ID конституенты отображаются ее атрибуты

- столбец "Описание" содержит один из непустых текстовых атрибутов

-

Статусы

- { [... mapStatusInfo.values()].map((info, index) => { - return (

- - {info.text} - - - - - {info.tooltip} - -

); - })} +
@@ -241,7 +231,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe />
-
+
cst.id)); }, [setSelected]); - const columns = useMemo(() => - [ - { - name: 'ID', - id: 'id', - selector: (cst: IConstituenta) => cst.id, - omit: true - }, - { - name: 'Имя', - id: 'alias', - selector: (cst: IConstituenta) => cst.alias, - cell: (cst: IConstituenta) => { - const info = mapStatusInfo.get(cst.status)!; - return (<> -
- {cst.alias} -
- -

Статус: {info.tooltip}

-
- ); - }, - width: '65px', - maxWidth: '65px', - reorder: true, - }, - { - name: 'Тип', - id: 'type', - cell: (cst: IConstituenta) =>
{getCstTypificationLabel(cst)}
, - width: '175px', - maxWidth: '175px', - wrap: true, - reorder: true, - hide: 1600 - }, - { - name: 'Термин', - id: 'term', - selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '', - width: '350px', - minWidth: '150px', - maxWidth: '350px', - wrap: true, - reorder: true - }, - { - name: 'Формальное определение', - id: 'expression', - selector: (cst: IConstituenta) => cst.definition?.formal ?? '', - minWidth: '300px', - maxWidth: '500px', - grow: 2, - wrap: true, - reorder: true - }, - { - name: 'Текстовое определение', - id: 'definition', - cell: (cst: IConstituenta) => ( -
- {cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''} + const columns = useMemo( + () => [ + { + name: 'ID', + id: 'id', + selector: (cst: IConstituenta) => cst.id, + omit: true + }, + { + name: 'Имя', + id: 'alias', + selector: (cst: IConstituenta) => cst.alias, + cell: (cst: IConstituenta) => { + const info = mapStatusInfo.get(cst.status)!; + return (<> +
+ {cst.alias}
- ), - minWidth: '200px', - grow: 2, - wrap: true, - reorder: true + +

Статус: {info.tooltip}

+
+ ); }, - { - name: 'Конвенция / Комментарий', - id: 'convention', - cell: (cst: IConstituenta) =>
{cst.convention ?? ''}
, - minWidth: '100px', - wrap: true, - reorder: true, - hide: 1800 - } - ], [] - ); + width: '65px', + maxWidth: '65px', + reorder: true, + }, + { + name: 'Тип', + id: 'type', + cell: (cst: IConstituenta) =>
{getCstTypificationLabel(cst)}
, + width: '175px', + maxWidth: '175px', + wrap: true, + reorder: true, + hide: 1600 + }, + { + name: 'Термин', + id: 'term', + selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '', + width: '350px', + minWidth: '150px', + maxWidth: '350px', + wrap: true, + reorder: true + }, + { + name: 'Формальное определение', + id: 'expression', + selector: (cst: IConstituenta) => cst.definition?.formal ?? '', + minWidth: '300px', + maxWidth: '500px', + grow: 2, + wrap: true, + reorder: true + }, + { + name: 'Текстовое определение', + id: 'definition', + cell: (cst: IConstituenta) => ( +
+ {cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''} +
+ ), + minWidth: '200px', + grow: 2, + wrap: true, + reorder: true + }, + { + name: 'Конвенция / Комментарий', + id: 'convention', + cell: (cst: IConstituenta) =>
{cst.convention ?? ''}
, + minWidth: '100px', + wrap: true, + reorder: true, + hide: 1800 + } + ], []); return (
@@ -314,20 +314,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)

Клик на квадрат слева - выделение конституенты

Alt + вверх/вниз - движение конституент

Delete - удаление конституент

-

Alt + 1-6, Q,W - добавление конституент

+

Alt + 1-6,Q,W - добавление конституент

-

Статусы

- { [... mapStatusInfo.values()].map(info => { - return (

- - {info.text} - - - - - {info.tooltip} - -

); - })} +
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx index 13645b8d..781dd72d 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx @@ -1,29 +1,117 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, + GraphNode, LayoutTypes, lightTheme, Sphere, useSelection +} from 'reagraph'; import Button from '../../components/Common/Button'; import Checkbox from '../../components/Common/Checkbox'; import ConceptSelect from '../../components/Common/ConceptSelect'; -import { ArrowsRotateIcon } from '../../components/Icons'; +import ConceptTooltip from '../../components/Common/ConceptTooltip'; +import Divider from '../../components/Common/Divider'; +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, DumpBinIcon, FilterCogIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons'; import { useRSForm } from '../../context/RSFormContext'; import { useConceptTheme } from '../../context/ThemeContext'; import useLocalStorage from '../../hooks/useLocalStorage'; -import { resources } from '../../utils/constants'; +import { prefixes, resources } from '../../utils/constants'; import { Graph } from '../../utils/Graph'; -import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI'; +import { CstType, IConstituenta } from '../../utils/models'; +import { getCstClassColor, getCstStatusColor, + GraphColoringSelector, GraphLayoutSelector, + mapColoringLabels, mapLayoutLabels +} from '../../utils/staticUI'; +import DlgGraphOptions from './DlgGraphOptions'; +import ConstituentaTooltip from './elements/ConstituentaTooltip'; -function EditorTermGraph() { - const { schema } = useRSForm(); +export type ColoringScheme = 'none' | 'status' | 'type'; +const TREE_SIZE_MILESTONE = 50; + +function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, darkMode: boolean): string { + if (coloringScheme === 'type') { + return getCstClassColor(cst.cstClass, darkMode); + } + if (coloringScheme === 'status') { + return getCstStatusColor(cst.status, darkMode); + } + return (darkMode ? '#7a8c9e' :'#7ca0ab'); +} + +export interface GraphEditorParams { + noHermits: boolean + noTransitive: boolean + noTemplates: boolean + noTerms: boolean + + allowBase: boolean + allowStruct: boolean + allowTerm: boolean + allowAxiom: boolean + allowFunction: boolean + allowPredicate: boolean + allowConstant: boolean + allowTheorem: boolean +} + +interface EditorTermGraphProps { + onOpenEdit: (cstID: number) => void + onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void + onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void +} + +function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) { + const { schema, isEditable } = useRSForm(); const { darkMode, noNavigation } = useConceptTheme(); + const [ layout, setLayout ] = useLocalStorage('graph_layout', 'treeTd2d'); - - const [ filtered, setFiltered ] = useState(new Graph()); + const [ coloringScheme, setColoringScheme ] = useLocalStorage('graph_coloring', 'none'); const [ orbit, setOrbit ] = useState(false); + const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true); const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false); + const [ noTemplates, setNoTemplates ] = useLocalStorage('graph_no_templates', false); + const [ noTerms, setNoTerms ] = useLocalStorage('graph_no_terms', false); + const [ allowBase, setAllowBase ] = useLocalStorage('graph_allow_base', true); + const [ allowStruct, setAllowStruct ] = useLocalStorage('graph_allow_struct', true); + const [ allowTerm, setAllowTerm ] = useLocalStorage('graph_allow_term', true); + const [ allowAxiom, setAllowAxiom ] = useLocalStorage('graph_allow_axiom', true); + const [ allowFunction, setAllowFunction ] = useLocalStorage('function', true); + const [ allowPredicate, setAllowPredicate ] = useLocalStorage('graph_allow_predicate', true); + const [ allowConstant, setAllowConstant ] = useLocalStorage('graph_allow_constant', true); + const [ allowTheorem, setAllowTheorem ] = useLocalStorage('graph_allow_theorem', true); + + const [ filtered, setFiltered ] = useState(new Graph()); + const [ dismissed, setDismissed ] = useState([]); + 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( + () => { + return schema?.items.find(cst => String(cst.id) == hoverID); + }, [schema?.items, hoverID]); - useEffect(() => { + const is3D = useMemo(() => layout.includes('3d'), [layout]); + const allowedTypes: CstType[] = useMemo( + () => { + const result: CstType[] = []; + if (allowBase) result.push(CstType.BASE); + if (allowStruct) result.push(CstType.STRUCTURED); + if (allowTerm) result.push(CstType.TERM); + if (allowAxiom) result.push(CstType.AXIOM); + if (allowFunction) result.push(CstType.FUNCTION); + if (allowPredicate) result.push(CstType.PREDICATE); + if (allowConstant) result.push(CstType.CONSTANT); + if (allowTheorem) result.push(CstType.THEOREM); + return result; + }, [allowBase, allowStruct, allowTerm, allowAxiom, allowFunction, allowPredicate, allowConstant, allowTheorem]); + + useLayoutEffect( + () => { if (!schema) { setFiltered(new Graph()); return; @@ -35,10 +123,46 @@ function EditorTermGraph() { if (noTransitive) { graph.transitiveReduction(); } + if (noTemplates) { + schema.items.forEach(cst => { + if (cst.isTemplate) { + graph.foldNode(cst.id); + } + }); + } + if (allowedTypes.length < Object.values(CstType).length) { + schema.items.forEach(cst => { + if (!allowedTypes.includes(cst.cstType)) { + graph.foldNode(cst.id); + } + }); + } + const newDismissed: number[] = []; + schema.items.forEach(cst => { + if (!graph.nodes.has(cst.id)) { + newDismissed.push(cst.id); + } + }); setFiltered(graph); - }, [schema, noHermits, noTransitive]); + setDismissed(newDismissed); + setSelectedDismissed([]); + setHoverID(undefined); + }, [schema, noHermits, noTransitive, noTemplates, allowedTypes, toggleUpdate]); - const nodes: GraphNode[] = useMemo(() => { + function toggleDismissed(cstID: number) { + setSelectedDismissed(prev => { + const index = prev.findIndex(id => cstID == id); + if (index !== -1) { + prev.splice(index, 1); + } else { + prev.push(cstID); + } + return [... prev]; + }); + } + + const nodes: GraphNode[] = useMemo( + () => { const result: GraphNode[] = []; if (!schema) { return result; @@ -48,14 +172,16 @@ function EditorTermGraph() { if (cst) { result.push({ id: String(node.id), - label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias + fill: getCstNodeColor(cst, coloringScheme, darkMode), + label: cst.term.resolved && !noTerms ? `${cst.alias}: ${cst.term.resolved}` : cst.alias }); } }); return result; - }, [schema, filtered.nodes]); + }, [schema, coloringScheme, filtered.nodes, darkMode, noTerms]); - const edges: GraphEdge[] = useMemo(() => { + const edges: GraphEdge[] = useMemo( + () => { const result: GraphEdge[] = []; let edgeID = 1; filtered.nodes.forEach(source => { @@ -70,15 +196,11 @@ function EditorTermGraph() { }); return result; }, [filtered.nodes]); - - const handleCenter = useCallback(() => { - graphRef.current?.resetControls(); - graphRef.current?.centerGraph(); - }, []); - + const { selections, actives, onNodeClick, + clearSelections, onCanvasClick, onNodePointerOver, onNodePointerOut @@ -87,52 +209,292 @@ function EditorTermGraph() { nodes, edges, type: 'multi', // 'single' | 'multi' | 'multiModifier' - pathSelectionType: 'all', + pathSelectionType: 'out', + pathHoverType: 'all', focusOnSelect: false }); - const canvasSize = !noNavigation ? - 'w-[1240px] h-[736px] 2xl:w-[1880px] 2xl:h-[746px]' - : 'w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[810px]'; + const allSelected: string[] = useMemo( + () => { + return [ ... selectedDismissed.map(id => String(id)), ... selections]; + }, [selectedDismissed, selections]); + const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]); - return (<> -
-
-
+ const handleRecreate = useCallback( + () => { + graphRef.current?.resetControls(); + graphRef.current?.centerGraph(); + }, []); + + const handleHoverIn = useCallback( + (node: GraphNode) => { + setHoverID(node.id); + if (onNodePointerOver) onNodePointerOver(node); + }, [onNodePointerOver]); + + const handleHoverOut = useCallback( + (node: GraphNode) => { + setHoverID(undefined); + if (onNodePointerOut) onNodePointerOut(node); + }, [onNodePointerOut]); + + const handleNodeClick = useCallback( + (node: GraphNode) => { + if (selections.includes(node.id)) { + onOpenEdit(Number(node.id)); + return; + } + if (onNodeClick) onNodeClick(node); + }, [onNodeClick, selections, onOpenEdit]); + + const handleCanvasClick = useCallback( + (event: MouseEvent) => { + setSelectedDismissed([]); + 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, + noTemplates: noTemplates, + noTransitive: noTransitive, + noTerms: noTerms, + + allowBase: allowBase, + allowStruct: allowStruct, + allowTerm: allowTerm, + allowAxiom: allowAxiom, + allowFunction: allowFunction, + allowPredicate: allowPredicate, + allowConstant: allowConstant, + allowTheorem: allowTheorem + } + } + + const handleChangeOptions = useCallback( + (params: GraphEditorParams) => { + setNoHermits(params.noHermits); + setNoTransitive(params.noTransitive); + setNoTemplates(params.noTemplates); + setNoTerms(params.noTerms); + + setAllowBase(params.allowBase); + setAllowStruct(params.allowStruct); + setAllowTerm(params.allowTerm); + setAllowAxiom(params.allowAxiom); + setAllowFunction(params.allowFunction); + setAllowPredicate(params.allowPredicate); + setAllowConstant(params.allowConstant); + setAllowTheorem(params.allowTheorem); + }, [setNoHermits, setNoTransitive, setNoTemplates, + setAllowBase, setAllowStruct, setAllowTerm, setAllowAxiom, setAllowFunction, + setAllowPredicate, setAllowConstant, setAllowTheorem, setNoTerms]); + + const canvasWidth = useMemo( + () => { + return 'calc(100vw - 14.6rem)'; + }, []); + + const canvasHeight = useMemo( + () => { + return !noNavigation ? + 'calc(100vh - 13rem)' + : 'calc(100vh - 2rem)'; + }, [noNavigation]); + + const dismissedStyle = useCallback( + (cstID: number) => { + return selectedDismissed.includes(cstID) ? {outlineWidth: '2px', outlineStyle: 'solid'}: {}; + }, [selectedDismissed]); + + return ( +
+ {showOptions && + setShowOptions(false)} + initial={getOptions()} + onConfirm={handleChangeOptions} + />} +
+ {hoverCst && +
+ +
} + +
+
+ Выбраны + + {allSelected.length} из {schema?.stats?.count_all ?? 0} + +
+
+ } + disabled={!isEditable || nothingSelected} + onClick={handleDeleteCst} + /> + } + disabled={!isEditable} + onClick={handleCreateCst} + /> +
+
+
- setOrbit(event.target.checked) } + { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }} /> setNoHermits(event.target.checked) } + label='Скрыть текст' + value={noTerms} + onChange={ event => setNoTerms(event.target.checked) } /> setNoTransitive(event.target.checked) } /> + setOrbit(event.target.checked) } + /> + + + +
+

Скрытые конституенты

+
+ {dismissed.map(cstID => { + const cst = schema!.items.find(cst => cst.id === cstID)!; + const adjustedColoring = coloringScheme === 'none' ? 'status': coloringScheme; + return (<> +
toggleDismissed(cstID)} + onDoubleClick={() => onOpenEdit(cstID)} + > + {cst.alias} +
+ + ); + })} +
+
-
-
-
+
+
+
+
+ +
+ } + tooltip='Пересоздать граф' + onClick={handleRecreate} + /> +
+ +
+
+

Настройка графа

+

Цвет - выбор правила покраски узлов

+

Граф - выбор модели расположения узлов

+

Удалить несвязанные - скрыть одинокие вершины

+

Транзитивная редукция - скрыть транзитивные пути

+ + + + +
+ +
+

Горячие клавиши

+

Клик на конституенту - выделение, включая скрытые конституенты

+

Довйной клик - редактирование конституенты

+

Delete - удалить выбранные

+ + + + +
+
+
( @@ -156,7 +520,7 @@ function EditorTermGraph() { />
- ); +
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index 87e673de..959835a1 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -40,7 +40,7 @@ function RSTabs() { const { destroySchema } = useLibrary(); const [activeTab, setActiveTab] = useState(RSTabsList.CARD); - const [activeID, setActiveID] = useState(undefined) + const [activeID, setActiveID] = useState(undefined); const [showUpload, setShowUpload] = useState(false); const [showClone, setShowClone] = useState(false); @@ -223,7 +223,7 @@ function RSTabs() { defaultFocus={true} selectedTabClassName='font-bold' > - + setShowClone(true)} @@ -264,7 +264,11 @@ function RSTabs() { - + } diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx index 286483dd..0fc442f9 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/elements/ConstituentaTooltip.tsx @@ -1,6 +1,6 @@ import ConceptTooltip from '../../../components/Common/ConceptTooltip'; +import InfoConstituenta from '../../../components/Help/InfoConstituenta'; import { IConstituenta } from '../../../utils/models'; -import { getCstTypificationLabel } from '../../../utils/staticUI'; interface ConstituentaTooltipProps { data: IConstituenta @@ -13,12 +13,7 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) { anchorSelect={anchor} className='max-w-[25rem] min-w-[25rem]' > -

Конституента {data.alias}

-

Типизация: {getCstTypificationLabel(data)}

-

Термин: {data.term.resolved || data.term.raw}

- {data.definition.formal &&

Выражение: {data.definition.formal}

} - {data.definition.text.resolved &&

Определение: {data.definition.text.resolved}

} - {data.convention &&

Конвенция: {data.convention}

} + ); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx index 6769fc69..f2b49c72 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx @@ -24,7 +24,7 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) { const data = mapStatusInfo.get(status)!; return (
+ className={`text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle ${data.color}`}> Статус: [ {data.text} ]
) diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx index 00e5727c..5c215574 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx @@ -158,7 +158,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: onChange={setFilterMatch} /> setFilterText(event.target.value)} diff --git a/rsconcept/frontend/src/utils/Graph.test.ts b/rsconcept/frontend/src/utils/Graph.test.ts index 80861ed8..063b42f4 100644 --- a/rsconcept/frontend/src/utils/Graph.test.ts +++ b/rsconcept/frontend/src/utils/Graph.test.ts @@ -45,6 +45,16 @@ describe('Testing Graph editing', () => { expect(graph.hasEdge(4, 1)).toBeFalsy(); }); + test('folding node redirectes edges', () => { + const graph = new Graph([[1, 3], [2, 3], [3, 4], [3, 5], [3, 3]]); + graph.foldNode(3); + expect(graph.hasNode(3)).toBeFalsy(); + expect(graph.hasEdge(1, 4)).toBeTruthy(); + expect(graph.hasEdge(1, 5)).toBeTruthy(); + expect(graph.hasEdge(2, 4)).toBeTruthy(); + expect(graph.hasEdge(2, 5)).toBeTruthy(); + }); + test('removing isolated nodes', () => { const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9], [7], [8]]); graph.removeIsolated() @@ -53,7 +63,7 @@ describe('Testing Graph editing', () => { test('transitive reduction', () => { const graph = new Graph([[1, 3], [1, 2], [2, 3]]); - graph.transitiveReduction() + graph.transitiveReduction(); expect(graph.hasEdge(1, 2)).toBeTruthy(); expect(graph.hasEdge(2, 3)).toBeTruthy(); expect(graph.hasEdge(1, 3)).toBeFalsy(); diff --git a/rsconcept/frontend/src/utils/Graph.ts b/rsconcept/frontend/src/utils/Graph.ts index c060c3d9..4e873e6d 100644 --- a/rsconcept/frontend/src/utils/Graph.ts +++ b/rsconcept/frontend/src/utils/Graph.ts @@ -67,6 +67,10 @@ export class Graph { return node; } + hasNode(target: number): boolean { + return !!this.nodes.get(target); + } + removeNode(target: number): GraphNode | null { const nodeToRemove = this.nodes.get(target); if (!nodeToRemove) { @@ -80,6 +84,19 @@ export class Graph { return nodeToRemove; } + foldNode(target: number): GraphNode | null { + const nodeToRemove = this.nodes.get(target); + if (!nodeToRemove) { + return null; + } + nodeToRemove.inputs.forEach(input => { + nodeToRemove.outputs.forEach(output => { + this.addEdge(input, output); + }) + }); + return this.removeNode(target); + } + removeIsolated(): GraphNode[] { const result: GraphNode[] = []; this.nodes.forEach(node => { diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 452a0ad8..eb598700 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -28,5 +28,6 @@ export const resources = { } export const prefixes = { - cst_list: 'cst-list-' + cst_list: 'cst-list-', + cst_status_list: 'cst-status-list-' } diff --git a/rsconcept/frontend/src/utils/models.ts b/rsconcept/frontend/src/utils/models.ts index c26f2214..62a7e228 100644 --- a/rsconcept/frontend/src/utils/models.ts +++ b/rsconcept/frontend/src/utils/models.ts @@ -101,6 +101,13 @@ export enum CstType { THEOREM = 'theorem' } +export enum CstClass { + BASIC = 'basic', + DERIVED = 'derived', + STATEMENT = 'statement', + TEMPLATE = 'template' +} + export interface IConstituenta { id: number alias: string @@ -118,7 +125,9 @@ export interface IConstituenta { resolved: string } } + cstClass: CstClass status: ExpressionStatus + isTemplate: boolean parse: { status: ParsingStatus valueClass: ValueClass @@ -230,12 +239,12 @@ export enum EditMode { // RSExpression status export enum ExpressionStatus { - UNDEFINED = 0, - UNKNOWN, - INCORRECT, - INCALCULABLE, - PROPERTY, - VERIFIED + UNDEFINED = 'undefined', + UNKNOWN = 'unknown', + INCORRECT = 'incorrect', + INCALCULABLE = 'incalculable', + PROPERTY = 'property', + VERIFIED = 'verified' } // Dependency mode for schema analysis @@ -274,7 +283,28 @@ export function inferStatus(parse?: ParsingStatus, value?: ValueClass): Expressi if (value === ValueClass.PROPERTY) { return ExpressionStatus.PROPERTY; } - return ExpressionStatus.VERIFIED + return ExpressionStatus.VERIFIED; +} + +export function inferTemplate(expression: string): boolean { + const match = expression.match(/R\d+/g); + return (match && match?.length > 0) ?? false; +} + +export function inferClass(type: CstType, isTemplate: boolean): CstClass { + if (isTemplate) { + return CstClass.TEMPLATE; + } + switch (type) { + case CstType.BASE: return CstClass.BASIC; + case CstType.CONSTANT: return CstClass.BASIC; + case CstType.STRUCTURED: return CstClass.BASIC; + case CstType.TERM: return CstClass.DERIVED; + case CstType.FUNCTION: return CstClass.DERIVED; + case CstType.AXIOM: return CstClass.STATEMENT; + case CstType.PREDICATE: return CstClass.DERIVED; + case CstType.THEOREM: return CstClass.STATEMENT; + } } export function extractGlobals(expression: string): Set { @@ -336,6 +366,8 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm { } result.items.forEach(cst => { cst.status = inferStatus(cst.parse.status, cst.parse.valueClass); + cst.isTemplate = inferTemplate(cst.definition.formal); + cst.cstClass = inferClass(cst.cstType, cst.isTemplate); result.graph.addNode(cst.id); const dependencies = extractGlobals(cst.definition.formal); dependencies.forEach(value => { diff --git a/rsconcept/frontend/src/utils/staticUI.ts b/rsconcept/frontend/src/utils/staticUI.ts index ba884892..97ee8dde 100644 --- a/rsconcept/frontend/src/utils/staticUI.ts +++ b/rsconcept/frontend/src/utils/staticUI.ts @@ -1,7 +1,8 @@ import { LayoutTypes } from 'reagraph'; +import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph'; import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums'; -import { CstMatchMode, CstType, DependencyMode,ExpressionStatus, IConstituenta, +import { CstClass, CstMatchMode, CstType, DependencyMode, ExpressionStatus, IConstituenta, IFunctionArg,IRSErrorDescription, IRSForm, ISyntaxTreeNode, ParsingStatus, ValueClass } from './models'; @@ -11,7 +12,7 @@ export interface IRSButtonData { tooltip: string } -export interface IStatusInfo { +export interface IFormatInfo { text: string color: string tooltip: string @@ -250,22 +251,6 @@ export const CstTypeSelector = (Object.values(CstType)).map( return { value: type, label: getCstTypeLabel(type) }; }); -export const mapLayoutLabels: Map = new Map([ - ['forceatlas2', 'Атлас 2D'], - ['forceDirected2d', 'Силы 2D'], - ['forceDirected3d', 'Силы 3D'], - ['treeTd2d', 'ДеревоВерт 2D'], - ['treeTd3d', 'ДеревоВерт 3D'], - ['treeLr2d', 'ДеревоГор 2D'], - ['treeLr3d', 'ДеревоГор 3D'], - ['radialOut2d', 'Радиальная 2D'], - ['radialOut3d', 'Радиальная 3D'], - ['circular2d', 'Круговая'], - ['hierarchicalTd', 'ИерархияВерт'], - ['hierarchicalLr', 'ИерархияГор'], - ['nooverlap', 'Без перекрытия'] -]); - export function getCstCompareLabel(mode: CstMatchMode): string { switch(mode) { case CstMatchMode.ALL: return 'везде'; @@ -288,22 +273,61 @@ export function getDependencyLabel(mode: DependencyMode): string { } export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [ - { value: 'forceatlas2', label: 'Атлас 2D'}, - { value: 'forceDirected2d', label: 'Силы 2D'}, - { value: 'forceDirected3d', label: 'Силы 3D'}, - { value: 'treeTd2d', label: 'ДеревоВ 2D'}, - { value: 'treeTd3d', label: 'ДеревоВ 3D'}, - { value: 'treeLr2d', label: 'ДеревоГ 2D'}, - { value: 'treeLr3d', label: 'ДеревоГ 3D'}, - { value: 'radialOut2d', label: 'Радиальная 2D'}, - { value: 'radialOut3d', label: 'Радиальная 3D'}, + { value: 'treeTd2d', label: 'Граф: ДеревоВ 2D'}, + { value: 'treeTd3d', label: 'Граф: ДеревоВ 3D'}, + { value: 'forceatlas2', label: 'Граф: Атлас 2D'}, + { value: 'forceDirected2d', label: 'Граф: Силы 2D'}, + { value: 'forceDirected3d', label: 'Граф: Силы 3D'}, + { value: 'treeLr2d', label: 'Граф: ДеревоГ 2D'}, + { value: 'treeLr3d', label: 'Граф: ДеревоГ 3D'}, + { value: 'radialOut2d', label: 'Граф: Радиальная 2D'}, + { value: 'radialOut3d', label: 'Граф: Радиальная 3D'}, // { value: 'circular2d', label: 'circular2d'}, // { value: 'nooverlap', label: 'nooverlap'}, // { value: 'hierarchicalTd', label: 'hierarchicalTd'}, // { value: 'hierarchicalLr', label: 'hierarchicalLr'} ]; -export const mapStatusInfo: Map = new Map([ +export const mapLayoutLabels: Map = new Map([ + ['forceatlas2', 'Граф: Атлас 2D'], + ['forceDirected2d', 'Граф: Силы 2D'], + ['forceDirected3d', 'Граф: Силы 3D'], + ['treeTd2d', 'Граф: ДеревоВерт 2D'], + ['treeTd3d', 'Граф: ДеревоВерт 3D'], + ['treeLr2d', 'Граф: ДеревоГор 2D'], + ['treeLr3d', 'Граф: ДеревоГор 3D'], + ['radialOut2d', 'Граф: Радиальная 2D'], + ['radialOut3d', 'Граф: Радиальная 3D'], + ['circular2d', 'Граф: Круговая'], + ['hierarchicalTd', 'Граф: ИерархияВерт'], + ['hierarchicalLr', 'Граф: ИерархияГор'], + ['nooverlap', 'Граф: Без перекрытия'] +]); + +export const mapColoringLabels: Map = new Map([ + ['none', 'Цвет: моно'], + ['status', 'Цвет: статус'], + ['type', 'Цвет: класс'], +]); + +export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [ + { value: 'none', label: 'Цвет: моно'}, + { value: 'status', label: 'Цвет: статус'}, + { value: 'type', label: 'Цвет: класс'}, +]; + +export function getCstStatusColor(status: ExpressionStatus, darkMode: boolean): string { + switch (status) { + case ExpressionStatus.VERIFIED: return darkMode ? '#2b8000': '#aaff80'; + case ExpressionStatus.INCORRECT: return darkMode ? '#592b2b': '#ffc9c9'; + case ExpressionStatus.INCALCULABLE: return darkMode ? '#964600': '#ffbb80'; + case ExpressionStatus.PROPERTY: return darkMode ? '#36899e': '#a5e9fa'; + case ExpressionStatus.UNKNOWN: return darkMode ? '#1e00b3': '#b3bdff'; + case ExpressionStatus.UNDEFINED: return darkMode ? '#1e00b3': '#b3bdff'; + } +} + +export const mapStatusInfo: Map = new Map([ [ ExpressionStatus.VERIFIED, { text: 'ок', color: 'bg-[#aaff80] dark:bg-[#2b8000]', @@ -317,7 +341,7 @@ export const mapStatusInfo: Map = new Map([ [ ExpressionStatus.INCALCULABLE, { text: 'невыч', color: 'bg-[#ffbb80] dark:bg-[#964600]', - tooltip: 'выражение не вычислимо (экспоненциальная сложность)' + tooltip: 'выражение не вычислимо' }], [ ExpressionStatus.PROPERTY, { text: 'св-во', @@ -336,6 +360,38 @@ export const mapStatusInfo: Map = new Map([ }], ]); +export function getCstClassColor(cstClass: CstClass, darkMode: boolean): string { + switch (cstClass) { + case CstClass.TEMPLATE: return darkMode ? '#36899e': '#a5e9fa'; + case CstClass.BASIC: return darkMode ? '#2b8000': '#aaff80'; + case CstClass.DERIVED: return darkMode ? '#1e00b3': '#b3bdff'; + case CstClass.STATEMENT: return darkMode ? '#592b2b': '#ffc9c9'; + } +} + +export const mapCstClassInfo: Map = new Map([ + [ CstClass.BASIC, { + text: 'базовый', + color: 'bg-[#aaff80] dark:bg-[#2b8000]', + tooltip: 'неопределяемое понятие, требует конвенции' + }], + [ CstClass.DERIVED, { + text: 'производный', + color: 'bg-[#b3bdff] dark:bg-[#1e00b3]', + tooltip: 'выводимое понятие, задаваемое определением' + }], + [ CstClass.STATEMENT, { + text: 'утверждение', + color: 'bg-[#ffc9c9] dark:bg-[#592b2b]', + tooltip: 'неопределяемое понятие, требует конвенции' + }], + [ CstClass.TEMPLATE, { + text: 'шаблон', + color: 'bg-[#a5e9fa] dark:bg-[#36899e]', + tooltip: 'параметризованный шаблон определения' + }], +]); + export function createAliasFor(type: CstType, schema: IRSForm): string { const prefix = getCstTypePrefix(type); if (!schema.items || schema.items.length <= 0) { @@ -370,6 +426,8 @@ export function getMockConstituenta(id: number, alias: string, type: CstType, co } }, status: ExpressionStatus.INCORRECT, + isTemplate: false, + cstClass: CstClass.DERIVED, parse: { status: ParsingStatus.INCORRECT, valueClass: ValueClass.INVALID,