Refactor EditorTermGraph

decouple responsibilities
This commit is contained in:
IRBorisov 2023-11-27 18:27:23 +03:00
parent b804586394
commit 50de0176f6
30 changed files with 881 additions and 708 deletions

View File

@ -1,105 +0,0 @@
import Checkbox from '../components/Common/Checkbox';
import Modal, { ModalProps } from '../components/Common/Modal';
import usePartialUpdate from '../hooks/usePartialUpdate';
import { GraphEditorParams } from '../models/miscelanious';
import { CstType } from '../models/rsform';
import { labelCstType } from '../utils/labels';
interface DlgGraphOptionsProps
extends Pick<ModalProps, 'hideWindow'> {
initial: GraphEditorParams
onConfirm: (params: GraphEditorParams) => void
}
function DlgGraphOptions({ hideWindow, initial, onConfirm } : DlgGraphOptionsProps) {
const [params, updateParams] = usePartialUpdate(initial);
const handleSubmit = () => {
hideWindow();
onConfirm(params);
};
return (
<Modal canSubmit
hideWindow={hideWindow}
title='Настройки графа термов'
onSubmit={handleSubmit}
submitText='Применить'
>
<div className='flex gap-2'>
<div className='flex flex-col gap-1'>
<h1>Преобразования</h1>
<Checkbox
label='Скрыть текст'
tooltip='Не отображать термины'
value={params.noTerms}
setValue={value => updateParams({noTerms: value})}
/>
<Checkbox
label='Скрыть несвязанные'
tooltip='Неиспользуемые конституенты'
value={params.noHermits}
setValue={value => updateParams({ noHermits: value})}
/>
<Checkbox
label='Скрыть шаблоны'
tooltip='Терм-функции и предикат-функции с параметризованными аргументами'
value={params.noTemplates}
setValue={value => updateParams({ noTemplates: value})}
/>
<Checkbox
label='Транзитивная редукция'
tooltip='Удалить связи, образующие транзитивные пути в графе'
value={params.noTransitive}
setValue={value => updateParams({ noTransitive: value})}
/>
</div>
<div className='flex flex-col gap-1'>
<h1>Типы конституент</h1>
<Checkbox
label={labelCstType(CstType.BASE)}
value={params.allowBase}
setValue={value => updateParams({ allowBase: value})}
/>
<Checkbox
label={labelCstType(CstType.STRUCTURED)}
value={params.allowStruct}
setValue={value => updateParams({ allowStruct: value})}
/>
<Checkbox
label={labelCstType(CstType.TERM)}
value={params.allowTerm}
setValue={value => updateParams({ allowTerm: value})}
/>
<Checkbox
label={labelCstType(CstType.AXIOM)}
value={params.allowAxiom}
setValue={value => updateParams({ allowAxiom: value})}
/>
<Checkbox
label={labelCstType(CstType.FUNCTION)}
value={params.allowFunction}
setValue={value => updateParams({ allowFunction: value})}
/>
<Checkbox
label={labelCstType(CstType.PREDICATE)}
value={params.allowPredicate}
setValue={value => updateParams({ allowPredicate: value})}
/>
<Checkbox
label={labelCstType(CstType.CONSTANT)}
value={params.allowConstant}
setValue={value => updateParams({ allowConstant: value})}
/>
<Checkbox
label={labelCstType(CstType.THEOREM)}
value={params.allowTheorem}
setValue={value => updateParams({ allowTheorem: value})}
/>
</div>
</div>
</Modal>
);
}
export default DlgGraphOptions;

View File

@ -0,0 +1,104 @@
import Checkbox from '../components/Common/Checkbox';
import Modal, { ModalProps } from '../components/Common/Modal';
import usePartialUpdate from '../hooks/usePartialUpdate';
import { GraphFilterParams } from '../models/miscelanious';
import { CstType } from '../models/rsform';
import { labelCstType } from '../utils/labels';
interface DlgGraphParamsProps
extends Pick<ModalProps, 'hideWindow'> {
initial: GraphFilterParams
onConfirm: (params: GraphFilterParams) => void
}
function DlgGraphParams({ hideWindow, initial, onConfirm } : DlgGraphParamsProps) {
const [params, updateParams] = usePartialUpdate(initial);
const handleSubmit = () => {
hideWindow();
onConfirm(params);
};
return (
<Modal canSubmit
hideWindow={hideWindow}
title='Настройки графа термов'
onSubmit={handleSubmit}
submitText='Применить'
>
<div className='flex gap-2'>
<div className='flex flex-col gap-1'>
<h1>Преобразования</h1>
<Checkbox
label='Скрыть текст'
tooltip='Не отображать термины'
value={params.noText}
setValue={value => updateParams({noText: value})}
/>
<Checkbox
label='Скрыть несвязанные'
tooltip='Неиспользуемые конституенты'
value={params.noHermits}
setValue={value => updateParams({ noHermits: value})}
/>
<Checkbox
label='Скрыть шаблоны'
tooltip='Терм-функции и предикат-функции с параметризованными аргументами'
value={params.noTemplates}
setValue={value => updateParams({ noTemplates: value})}
/>
<Checkbox
label='Транзитивная редукция'
tooltip='Удалить связи, образующие транзитивные пути в графе'
value={params.noTransitive}
setValue={value => updateParams({ noTransitive: value})}
/>
</div>
<div className='flex flex-col gap-1'>
<h1>Типы конституент</h1>
<Checkbox
label={labelCstType(CstType.BASE)}
value={params.allowBase}
setValue={value => updateParams({ allowBase: value})}
/>
<Checkbox
label={labelCstType(CstType.STRUCTURED)}
value={params.allowStruct}
setValue={value => updateParams({ allowStruct: value})}
/>
<Checkbox
label={labelCstType(CstType.TERM)}
value={params.allowTerm}
setValue={value => updateParams({ allowTerm: value})}
/>
<Checkbox
label={labelCstType(CstType.AXIOM)}
value={params.allowAxiom}
setValue={value => updateParams({ allowAxiom: value})}
/>
<Checkbox
label={labelCstType(CstType.FUNCTION)}
value={params.allowFunction}
setValue={value => updateParams({ allowFunction: value})}
/>
<Checkbox
label={labelCstType(CstType.PREDICATE)}
value={params.allowPredicate}
setValue={value => updateParams({ allowPredicate: value})}
/>
<Checkbox
label={labelCstType(CstType.CONSTANT)}
value={params.allowConstant}
setValue={value => updateParams({ allowConstant: value})}
/>
<Checkbox
label={labelCstType(CstType.THEOREM)}
value={params.allowTheorem}
setValue={value => updateParams({ allowTheorem: value})}
/>
</div>
</div>
</Modal>);
}
export default DlgGraphParams;

View File

@ -14,6 +14,11 @@ export enum DependencyMode {
EXPAND_INPUTS
}
/**
* Represents graph node coloring scheme.
*/
export type GraphColoringScheme = 'none' | 'status' | 'type';
/**
* Represents manuals topic.
*/
@ -69,11 +74,11 @@ export enum LibraryFilterStrategy {
/**
* Represents parameters for GraphEditor.
*/
export interface GraphEditorParams {
export interface GraphFilterParams {
noHermits: boolean
noTransitive: boolean
noTemplates: boolean
noTerms: boolean
noText: boolean
allowBase: boolean
allowStruct: boolean

View File

@ -1,20 +1,20 @@
import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import MiniButton from '../../components/Common/MiniButton';
import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea';
import HelpConstituenta from '../../components/Help/HelpConstituenta';
import { ArrowsRotateIcon, CloneIcon, DumpBinIcon, EditIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import RefsInput from '../../components/RefsInput';
import { useRSForm } from '../../context/RSFormContext';
import useWindowSize from '../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData } from '../../models/rsform';
import { SyntaxTree } from '../../models/rslang';
import { labelCstTypification } from '../../utils/labels';
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
import MiniButton from '../../../components/Common/MiniButton';
import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea';
import HelpConstituenta from '../../../components/Help/HelpConstituenta';
import { ArrowsRotateIcon, CloneIcon, DumpBinIcon, EditIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../../components/Icons';
import RefsInput from '../../../components/RefsInput';
import { useRSForm } from '../../../context/RSFormContext';
import useWindowSize from '../../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstRenameData, ICstUpdateData } from '../../../models/rsform';
import { SyntaxTree } from '../../../models/rslang';
import { labelCstTypification } from '../../../utils/labels';
import EditorRSExpression from './EditorRSExpression';
import ViewSideConstituents from './elements/ViewSideConstituents';
import ViewSideConstituents from './ViewSideConstituents';
// Max height of content for left enditor pane
const UNFOLDED_HEIGHT = '59.1rem';

View File

@ -2,22 +2,22 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import Button from '../../components/Common/Button';
import { ConceptLoader } from '../../components/Common/ConceptLoader';
import MiniButton from '../../components/Common/MiniButton';
import { ASTNetworkIcon } from '../../components/Icons';
import RSInput from '../../components/RSInput';
import { RSTextWrapper } from '../../components/RSInput/textEditing';
import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression';
import { IConstituenta } from '../../models/rsform';
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '../../models/rslang';
import { TokenID } from '../../models/rslang';
import { labelTypification } from '../../utils/labels';
import { getCstExpressionPrefix } from '../../utils/misc';
import ParsingResult from './elements/ParsingResult';
import RSEditorControls from './elements/RSEditorControls';
import StatusBar from './elements/StatusBar';
import Button from '../../../components/Common/Button';
import { ConceptLoader } from '../../../components/Common/ConceptLoader';
import MiniButton from '../../../components/Common/MiniButton';
import { ASTNetworkIcon } from '../../../components/Icons';
import RSInput from '../../../components/RSInput';
import { RSTextWrapper } from '../../../components/RSInput/textEditing';
import { useRSForm } from '../../../context/RSFormContext';
import useCheckExpression from '../../../hooks/useCheckExpression';
import { IConstituenta } from '../../../models/rsform';
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '../../../models/rslang';
import { TokenID } from '../../../models/rslang';
import { labelTypification } from '../../../utils/labels';
import { getCstExpressionPrefix } from '../../../utils/misc';
import ParsingResult from './ParsingResult';
import RSEditorControls from './RSEditControls';
import StatusBar from './StatusBar';
interface EditorRSExpressionProps {
id?: string

View File

@ -0,0 +1 @@
export { default } from './EditorConstituenta';

View File

@ -2,21 +2,21 @@ import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { toast } from 'react-toastify';
import Checkbox from '../../components/Common/Checkbox';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
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 TextInput from '../../components/Common/TextInput';
import HelpRSFormMeta from '../../components/Help/HelpRSFormMeta';
import { DownloadIcon, DumpBinIcon, HelpIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext';
import { useUsers } from '../../context/UsersContext';
import { LibraryItemType } from '../../models/library';
import { IRSFormCreateData } from '../../models/rsform';
import RSFormStats from './elements/RSFormStats';
import Checkbox from '../../../components/Common/Checkbox';
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
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 TextInput from '../../../components/Common/TextInput';
import HelpRSFormMeta from '../../../components/Help/HelpRSFormMeta';
import { DownloadIcon, DumpBinIcon, HelpIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../../components/Icons';
import { useAuth } from '../../../context/AuthContext';
import { useRSForm } from '../../../context/RSFormContext';
import { useUsers } from '../../../context/UsersContext';
import { LibraryItemType } from '../../../models/library';
import { IRSFormCreateData } from '../../../models/rsform';
import RSFormStats from './RSFormStats';
interface EditorRSFormProps {
onDestroy: () => void

View File

@ -0,0 +1 @@
export { default } from './EditorRSForm';

View File

@ -1,15 +1,15 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../components/DataTable';
import ConstituentaBadge from '../../components/Shared/ConstituentaBadge';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import useWindowSize from '../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../models/rsform'
import { prefixes } from '../../utils/constants';
import { labelCstTypification } from '../../utils/labels';
import RSItemsMenu from './elements/RSItemsMenu';
import DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../../components/DataTable';
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import useWindowSize from '../../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../../models/rsform'
import { prefixes } from '../../../utils/constants';
import { labelCstTypification } from '../../../utils/labels';
import RSItemsMenu from './RSListToolbar';
// Window width cutoff for columns
const COLUMN_DEFINITION_HIDE_THRESHOLD = 1000;
@ -18,14 +18,14 @@ const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
const columnHelper = createColumnHelper<IConstituenta>();
interface EditorItemsProps {
interface EditorRSListProps {
onOpenEdit: (cstID: number) => void
onTemplates: (selected: number[]) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
}
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorItemsProps) {
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
const { colors, mainHeight } = useConceptTheme();
const windowSize = useWindowSize();
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
@ -300,7 +300,8 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edit
Выбор {selected.length} из {schema?.stats?.count_all ?? 0}
</div>
<RSItemsMenu
selected={selected}
selectedCount={selected.length}
editorMode={isEditable}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
onClone={handleClone}
@ -344,4 +345,4 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edit
</div>);
}
export default EditorItems;
export default EditorRSList;

View File

@ -6,7 +6,6 @@ import DropdownButton from '../../../components/Common/DropdownButton';
import MiniButton from '../../../components/Common/MiniButton';
import HelpRSFormItems from '../../../components/Help/HelpRSFormItems';
import { ArrowDownIcon, ArrowDropdownIcon, ArrowUpIcon, CloneIcon, DiamondIcon, DumpBinIcon, HelpIcon, SmallPlusIcon,UpdateIcon } from '../../../components/Icons';
import { useRSForm } from '../../../context/RSFormContext';
import useDropdown from '../../../hooks/useDropdown';
import { CstType } from '../../../models/rsform';
import { prefixes } from '../../../utils/constants';
@ -14,7 +13,8 @@ import { labelCstType } from '../../../utils/labels';
import { getCstTypePrefix, getCstTypeShortcut } from '../../../utils/misc';
interface RSItemsMenuProps {
selected: number[]
editorMode?: boolean
selectedCount: number
onMoveUp: () => void
onMoveDown: () => void
@ -26,51 +26,50 @@ interface RSItemsMenuProps {
}
function RSItemsMenu({
selected,
selectedCount, editorMode,
onMoveUp, onMoveDown, onDelete, onClone, onCreate, onTemplates, onReindex
}: RSItemsMenuProps) {
const { isEditable } = useRSForm();
const insertMenu = useDropdown();
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
return (
<div className='flex items-center justify-center w-full pr-[9rem]'>
<MiniButton
tooltip='Переместить вверх'
icon={<ArrowUpIcon size={5}/>}
disabled={!isEditable || nothingSelected}
disabled={!editorMode || nothingSelected}
onClick={onMoveUp}
/>
<MiniButton
tooltip='Переместить вниз'
icon={<ArrowDownIcon size={5}/>}
disabled={!isEditable || nothingSelected}
disabled={!editorMode || nothingSelected}
onClick={onMoveDown}
/>
<MiniButton
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={5}/>}
disabled={!isEditable || nothingSelected}
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5}/>}
disabled={!editorMode || nothingSelected}
onClick={onDelete}
/>
<MiniButton
tooltip='Клонировать конституенту'
icon={<CloneIcon color={isEditable && selected.length === 1 ? 'text-success': ''} size={5}/>}
disabled={!isEditable || selected.length !== 1}
icon={<CloneIcon color={editorMode && selectedCount === 1 ? 'text-success': ''} size={5}/>}
disabled={!editorMode || selectedCount !== 1}
onClick={onClone}
/>
<MiniButton
tooltip='Добавить новую конституенту...'
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} size={5}/>}
disabled={!isEditable}
icon={<SmallPlusIcon color={editorMode ? 'text-success': ''} size={5}/>}
disabled={!editorMode}
onClick={() => onCreate()}
/>
<div ref={insertMenu.ref} className='flex justify-center'>
<MiniButton
tooltip='Добавить пустую конституенту'
icon={<ArrowDropdownIcon color={isEditable ? 'text-success': ''} size={5}/>}
disabled={!isEditable}
icon={<ArrowDropdownIcon color={editorMode ? 'text-success': ''} size={5}/>}
disabled={!editorMode}
onClick={insertMenu.toggle}
/>
{ insertMenu.isActive &&
@ -92,15 +91,15 @@ function RSItemsMenu({
<MiniButton
tooltip='Создать конституенту из шаблона'
icon={<DiamondIcon color={isEditable ? 'text-primary': ''} size={5}/>}
disabled={!isEditable}
icon={<DiamondIcon color={editorMode ? 'text-primary': ''} size={5}/>}
disabled={!editorMode}
onClick={onTemplates}
/>
<MiniButton
tooltip='Сброс имен: присвоить порядковые имена'
icon={<UpdateIcon color={isEditable ? 'text-primary': ''} size={5}/>}
disabled={!isEditable}
icon={<UpdateIcon color={editorMode ? 'text-primary': ''} size={5}/>}
disabled={!editorMode}
onClick={onReindex}
/>

View File

@ -0,0 +1 @@
export { default } from './EditorRSList';

View File

@ -1,510 +0,0 @@
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { GraphCanvas, GraphCanvasRef, GraphEdge,
GraphNode, LayoutTypes, Sphere, useSelection
} from 'reagraph';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import MiniButton from '../../components/Common/MiniButton';
import SelectSingle from '../../components/Common/SelectSingle';
import ConstituentaTooltip from '../../components/Help/ConstituentaTooltip';
import HelpTermGraph from '../../components/Help/HelpTermGraph';
import InfoConstituenta from '../../components/Help/InfoConstituenta';
import { ArrowsFocusIcon, DumpBinIcon, FilterIcon, HelpIcon, LetterAIcon, LetterALinesIcon, PlanetIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import DlgGraphOptions from '../../dialogs/DlgGraphOptions';
import useLocalStorage from '../../hooks/useLocalStorage';
import { GraphEditorParams } from '../../models/miscelanious';
import { CstType, IConstituenta, ICstCreateData } from '../../models/rsform';
import { graphDarkT, graphLightT, IColorTheme } from '../../utils/color';
import { colorbgCstClass } from '../../utils/color';
import { colorbgCstStatus } from '../../utils/color';
import { prefixes, resources, TIMEOUT_GRAPH_REFRESH } from '../../utils/constants';
import { Graph } from '../../utils/Graph';
import { mapLabelColoring } from '../../utils/labels';
import { mapLableLayout } from '../../utils/labels';
import { SelectorGraphLayout } from '../../utils/selectors';
import { SelectorGraphColoring } from '../../utils/selectors';
export type ColoringScheme = 'none' | 'status' | 'type';
const TREE_SIZE_MILESTONE = 50;
function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, colors: IColorTheme): string {
if (coloringScheme === 'type') {
return colorbgCstClass(cst.cst_class, colors);
}
if (coloringScheme === 'status') {
return colorbgCstStatus(cst.status, colors);
}
return '';
}
interface EditorTermGraphProps {
onOpenEdit: (cstID: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
}
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
const { schema, isEditable } = useRSForm();
const { darkMode, colors, noNavigation } = useConceptTheme();
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
const [ coloringScheme, setColoringScheme ] = useLocalStorage<ColoringScheme>('graph_coloring', 'type');
const [ orbit, setOrbit ] = useState(false);
const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true);
const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', true);
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<Graph>(new Graph());
const [ dismissed, setDismissed ] = useState<number[]>([]);
const [ selectedDismissed, setSelectedDismissed ] = useState<number[]>([]);
const graphRef = useRef<GraphCanvasRef | null>(null);
const [showOptions, setShowOptions] = useState(false);
const [toggleUpdate, setToggleUpdate] = useState(false);
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
const hoverCst = useMemo(
() => {
return schema?.items.find(cst => cst.id === hoverID);
}, [schema?.items, hoverID]);
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;
}
const graph = schema.graph.clone();
if (noHermits) {
graph.removeIsolated();
}
if (noTransitive) {
graph.transitiveReduction();
}
if (noTemplates) {
schema.items.forEach(cst => {
if (cst.is_template) {
graph.foldNode(cst.id);
}
});
}
if (allowedTypes.length < Object.values(CstType).length) {
schema.items.forEach(cst => {
if (!allowedTypes.includes(cst.cst_type)) {
graph.foldNode(cst.id);
}
});
}
const newDismissed: number[] = [];
schema.items.forEach(cst => {
if (!graph.nodes.has(cst.id)) {
newDismissed.push(cst.id);
}
});
setFiltered(graph);
setDismissed(newDismissed);
setSelectedDismissed([]);
setHoverID(undefined);
}, [schema, noHermits, noTransitive, noTemplates, allowedTypes, toggleUpdate]);
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;
}
filtered.nodes.forEach(node => {
const cst = schema.items.find(cst => cst.id === node.id);
if (cst) {
result.push({
id: String(node.id),
fill: getCstNodeColor(cst, coloringScheme, colors),
label: cst.term_resolved && !noTerms ? `${cst.alias}: ${cst.term_resolved}` : cst.alias
});
}
});
return result;
}, [schema, coloringScheme, filtered.nodes, noTerms, colors]);
const edges: GraphEdge[] = useMemo(
() => {
const result: GraphEdge[] = [];
let edgeID = 1;
filtered.nodes.forEach(source => {
source.outputs.forEach(target => {
result.push({
id: String(edgeID),
source: String(source.id),
target: String(target)
});
edgeID += 1;
});
});
return result;
}, [filtered.nodes]);
const {
selections, actives,
onNodeClick,
clearSelections,
onCanvasClick,
onNodePointerOver,
onNodePointerOut
} = useSelection({
ref: graphRef,
nodes,
edges,
type: 'multi', // 'single' | 'multi' | 'multiModifier'
pathSelectionType: 'out',
pathHoverType: 'all',
focusOnSelect: false
});
const allSelected: number[] = useMemo(
() => {
return [ ... selectedDismissed, ... selections.map(id => Number(id))];
}, [selectedDismissed, selections]);
const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]);
const handleResetViewpoint = useCallback(
() => {
graphRef.current?.resetControls(true);
graphRef.current?.centerGraph();
}, []);
const handleHoverIn = useCallback(
(node: GraphNode) => {
setHoverID(Number(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<HTMLDivElement>) {
if (!isEditable) {
return;
}
if (event.key === 'Delete' && allSelected.length > 0) {
event.preventDefault();
handleDeleteCst();
}
}
function handleCreateCst() {
if (!schema) {
return;
}
const data: ICstCreateData = {
insert_after: null,
cst_type: allSelected.length === 0 ? CstType.BASE: CstType.TERM,
alias: '',
term_raw: '',
definition_formal: allSelected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' '),
definition_raw: '',
convention: '',
term_forms: []
};
onCreateCst(data);
}
function handleDeleteCst() {
if (!schema) {
return;
}
onDeleteCst(allSelected, () => {
clearSelections();
setDismissed([]);
setSelectedDismissed([]);
setToggleUpdate(prev => !prev);
});
}
function handleChangeLayout(newLayout: LayoutTypes) {
if (newLayout === layout) {
return;
}
setLayout(newLayout);
setTimeout(() => {
handleResetViewpoint();
}, TIMEOUT_GRAPH_REFRESH);
}
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 - 1.1rem)';
}, []);
const canvasHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 9.8rem - 4px)'
: 'calc(100vh - 3rem - 4px)';
}, [noNavigation]);
const dismissedHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 28rem - 4px)'
: 'calc(100vh - 22.2rem - 4px)';
}, [noNavigation]);
const dismissedStyle = useCallback(
(cstID: number) => {
return selectedDismissed.includes(cstID) ? {outlineWidth: '2px', outlineStyle: 'solid'}: {};
}, [selectedDismissed]);
return (<>
{showOptions &&
<DlgGraphOptions
hideWindow={() => setShowOptions(false)}
initial={getOptions()}
onConfirm={handleChangeOptions}
/>}
{ allSelected.length > 0 &&
<div className='relative w-full z-pop'>
<div className='absolute top-0 left-0 px-2 select-none whitespace-nowrap small-caps clr-app'>
Выбор {allSelected.length} из {schema?.stats?.count_all ?? 0}
</div>
</div>}
<div className='relative w-full z-pop'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<MiniButton
tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='text-primary' size={5}/>}
onClick={() => setShowOptions(true)}
/>
<MiniButton
tooltip={ !noTerms ? 'Скрыть текст' : 'Отобразить текст' }
icon={ !noTerms ? <LetterALinesIcon color='text-success' size={5}/> : <LetterAIcon color='text-primary' size={5}/> }
onClick={() => setNoTerms(prev => !prev)}
/>
<MiniButton
tooltip='Новая конституента'
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} size={5}/>}
disabled={!isEditable}
onClick={handleCreateCst}
/>
<MiniButton
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={5}/>}
disabled={!isEditable || nothingSelected}
onClick={handleDeleteCst}
/>
<MiniButton
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
tooltip='Восстановить камеру'
onClick={handleResetViewpoint}
/>
<MiniButton
icon={<PlanetIcon color={ !is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
tooltip='Анимация вращения'
disabled={!is3D}
onClick={() => setOrbit(prev => !prev) }
/>
<div className='px-1 py-1' id='items-graph-help' >
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#items-graph-help'>
<div className='text-sm max-w-[calc(100vw-20rem)] z-tooltip'>
<HelpTermGraph />
</div>
</ConceptTooltip>
</div>
</div>
{hoverCst &&
<div className='relative'>
<InfoConstituenta
data={hoverCst}
className='absolute top-[1.6rem] left-[2.6rem] z-tooltip w-[25rem] min-h-[11rem] shadow-md overflow-y-auto border h-fit clr-app px-3'
/>
</div>}
<div className='relative z-pop'>
<div className='absolute top-0 left-0 flex flex-col max-w-[13.5rem] min-w-[13.5rem]'>
<div className='flex flex-col px-2 pb-2 mt-8 text-sm select-none h-fit'>
<div className='flex items-center w-full gap-1 text-sm'>
<SelectSingle
options={SelectorGraphColoring}
isSearchable={false}
placeholder='Выберите цвет'
value={coloringScheme ? { value: coloringScheme, label: mapLabelColoring.get(coloringScheme) } : null}
onChange={data => setColoringScheme(data?.value ?? SelectorGraphColoring[0].value)}
/>
</div>
<SelectSingle
className='w-full mt-1'
options={SelectorGraphLayout}
isSearchable={false}
placeholder='Способ расположения'
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}
onChange={data => handleChangeLayout(data?.value ?? SelectorGraphLayout[0].value)}
/>
</div>
{dismissed.length > 0 &&
<div className='flex flex-col text-sm ml-2 border clr-app max-w-[12.5rem] min-w-[12.5rem]'>
<p className='py-2 text-center'><b>Скрытые конституенты</b></p>
<div className='flex flex-wrap justify-center gap-2 pb-2 overflow-y-auto' style={{maxHeight: dismissedHeight}}>
{dismissed.map(cstID => {
const cst = schema!.items.find(cst => cst.id === cstID)!;
const adjustedColoring = coloringScheme === 'none' ? 'status': coloringScheme;
const id = `${prefixes.cst_hidden_list}${cst.alias}`
return (<div key={`wrap-${id}`}>
<div
key={id}
id={id}
className='w-fit min-w-[3rem] rounded-md text-center cursor-pointer select-none'
style={{
backgroundColor: getCstNodeColor(cst, adjustedColoring, colors),
...dismissedStyle(cstID)
}}
onClick={() => toggleDismissed(cstID)}
onDoubleClick={() => onOpenEdit(cstID)}
>
{cst.alias}
</div>
<ConstituentaTooltip
data={cst}
anchor={`#${id}`}
/>
</div>);
})}
</div>
</div>}
</div>
</div>
<div tabIndex={-1}
className='w-full h-full overflow-auto outline-none'
onKeyDown={handleKeyDown}
>
<div
className='relative'
style={{width: canvasWidth, height: canvasHeight}}
>
<GraphCanvas
draggable
ref={graphRef}
nodes={nodes}
edges={edges}
layoutType={layout}
selections={selections}
actives={actives}
onNodeClick={handleNodeClick}
onCanvasClick={handleCanvasClick}
onNodePointerOver={handleHoverIn}
onNodePointerOut={handleHoverOut}
cameraMode={ orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
layoutOverrides={
layout.includes('tree') ? { nodeLevelRatio: filtered.nodes.size < TREE_SIZE_MILESTONE ? 3 : 1 }
: undefined
}
labelFontUrl={resources.graph_font}
theme={darkMode ? graphDarkT : graphLightT}
renderNode={({ node, ...rest }) => (
<Sphere {...rest} node={node} />
)}
/>
</div>
</div></>);
}
export default EditorTermGraph;

View File

@ -0,0 +1,272 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
import InfoConstituenta from '../../../components/Help/InfoConstituenta';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import DlgGraphParams from '../../../dialogs/DlgGraphParams';
import useLocalStorage from '../../../hooks/useLocalStorage';
import { GraphColoringScheme, GraphFilterParams } from '../../../models/miscelanious';
import { CstType, ICstCreateData } from '../../../models/rsform';
import { colorbgGraphNode } from '../../../utils/color';
import { TIMEOUT_GRAPH_REFRESH } from '../../../utils/constants';
import GraphSidebar from './GraphSidebar';
import GraphToolbar from './GraphToolbar';
import TermGraph from './TermGraph';
import useGraphFilter from './useGraphFilter';
import ViewHidden from './ViewHidden';
interface EditorTermGraphProps {
onOpenEdit: (cstID: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
}
function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGraphProps) {
const { schema, isEditable } = useRSForm();
const { colors } = useConceptTheme();
const [toggleDataUpdate, setToggleDataUpdate] = useState(false);
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>('graph_filter', {
noHermits: true,
noTemplates: false,
noTransitive: true,
noText: false,
allowBase: true,
allowStruct: true,
allowTerm: true,
allowAxiom: true,
allowFunction: true,
allowPredicate: true,
allowConstant: true,
allowTheorem: true
});
const [showParamsDialog, setShowParamsDialog] = useState(false);
const filtered = useGraphFilter(schema, filterParams, toggleDataUpdate);
const [selectedGraph, setSelectedGraph] = useState<number[]>([]);
const [hidden, setHidden] = useState<number[]>([]);
const [selectedHidden, setSelectedHidden] = useState<number[]>([]);
const selected: number[] = useMemo(
() => {
return [...selectedHidden, ...selectedGraph];
}, [selectedHidden, selectedGraph]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const [layout, setLayout] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
const is3D = useMemo(() => layout.includes('3d'), [layout]);
const [coloringScheme, setColoringScheme] = useLocalStorage<GraphColoringScheme>('graph_coloring', 'type');
const [orbit, setOrbit] = useState(false);
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
const hoverCst = useMemo(
() => {
return schema?.items.find(cst => cst.id === hoverID);
}, [schema?.items, hoverID]);
const [toggleResetView, setToggleResetView] = useState(false);
const [toggleResetSelection, setToggleResetSelection] = useState(false);
useLayoutEffect(
() => {
if (!schema) {
return;
}
const newDismissed: number[] = [];
schema.items.forEach(cst => {
if (!filtered.nodes.has(cst.id)) {
newDismissed.push(cst.id);
}
});
setHidden(newDismissed);
setSelectedHidden([]);
setHoverID(undefined);
}, [schema, filtered, toggleDataUpdate]);
const nodes: GraphNode[] = useMemo(
() => {
const result: GraphNode[] = [];
if (!schema) {
return result;
}
filtered.nodes.forEach(node => {
const cst = schema.items.find(cst => cst.id === node.id);
if (cst) {
result.push({
id: String(node.id),
fill: colorbgGraphNode(cst, coloringScheme, colors),
label: cst.term_resolved && !filterParams.noText ? `${cst.alias}: ${cst.term_resolved}` : cst.alias
});
}
});
return result;
}, [schema, coloringScheme, filtered.nodes, filterParams.noText, colors]);
const edges: GraphEdge[] = useMemo(
() => {
const result: GraphEdge[] = [];
let edgeID = 1;
filtered.nodes.forEach(source => {
source.outputs.forEach(target => {
result.push({
id: String(edgeID),
source: String(source.id),
target: String(target)
});
edgeID += 1;
});
});
return result;
}, [filtered.nodes]);
function toggleDismissed(cstID: number) {
setSelectedHidden(prev => {
const index = prev.findIndex(id => cstID === id);
if (index !== -1) {
prev.splice(index, 1);
} else {
prev.push(cstID);
}
return [...prev];
});
}
function handleCreateCst() {
if (!schema) {
return;
}
const data: ICstCreateData = {
insert_after: null,
cst_type: selected.length === 0 ? CstType.BASE: CstType.TERM,
alias: '',
term_raw: '',
definition_formal: selected.map(id => schema.items.find(cst => cst.id === id)!.alias).join(' '),
definition_raw: '',
convention: '',
term_forms: []
};
onCreateCst(data);
}
function handleDeleteCst() {
if (!schema || selected.length === 0) {
return;
}
onDeleteCst(selected, () => {
setHidden([]);
setSelectedHidden([]);
setToggleResetSelection(prev => !prev);
setToggleDataUpdate(prev => !prev);
});
}
function handleChangeLayout(newLayout: LayoutTypes) {
if (newLayout === layout) {
return;
}
setLayout(newLayout);
setTimeout(() => {
setToggleResetView(prev => !prev);
}, TIMEOUT_GRAPH_REFRESH);
}
const handleChangeParams = useCallback(
(params: GraphFilterParams) => {
setFilterParams(params);
}, [setFilterParams]);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
// Hotkeys implementation
if (!isEditable) {
return;
}
if (event.key === 'Delete') {
event.preventDefault();
handleDeleteCst();
}
}
return (
<div tabIndex={-1} onKeyDown={handleKeyDown}>
{showParamsDialog ?
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/> : null}
{selected.length > 0 ?
<div className='relative w-full z-pop'>
<div className='absolute top-0 left-0 px-2 select-none whitespace-nowrap small-caps clr-app'>
Выбор {selected.length} из {schema?.stats?.count_all ?? 0}
</div>
</div> : null}
<GraphToolbar
editorMode={isEditable}
nothingSelected={nothingSelected}
is3D={is3D}
orbit={orbit}
noText={filterParams.noText}
showParamsDialog={() => setShowParamsDialog(true)}
onCreate={handleCreateCst}
onDelete={handleDeleteCst}
onResetViewpoint={() => setToggleResetView(prev => !prev)}
toggleOrbit={() => setOrbit(prev => !prev)}
toggleNoText={() => setFilterParams(
(prev) => ({
...prev,
noText: !prev.noText
})
)}
/>
{hoverCst ?
<div className='relative'>
<InfoConstituenta
data={hoverCst}
className='absolute top-[1.6rem] left-[2.6rem] z-tooltip w-[25rem] min-h-[11rem] shadow-md overflow-y-auto border h-fit clr-app px-3'
/>
</div> : null}
<div className='relative z-pop'>
<div className='absolute top-0 left-0 flex flex-col max-w-[13.5rem] min-w-[13.5rem]'>
<GraphSidebar
coloring={coloringScheme}
layout={layout}
setLayout={handleChangeLayout}
setColoring={setColoringScheme}
/>
<ViewHidden
items={hidden}
selected={selectedHidden}
schema={schema!}
coloringScheme={coloringScheme}
toggleSelection={toggleDismissed}
onEdit={onOpenEdit}
/>
</div>
</div>
<TermGraph
nodes={nodes}
edges={edges}
layout={layout}
is3D={is3D}
orbit={orbit}
setSelected={setSelectedGraph}
setHoverID={setHoverID}
onEdit={onOpenEdit}
onDeselect={() => setSelectedHidden([])}
toggleResetView={toggleResetView}
toggleResetSelection={toggleResetSelection}
/>
</div>);
}
export default EditorTermGraph;

View File

@ -0,0 +1,42 @@
import { LayoutTypes } from 'reagraph';
import SelectSingle from '../../../components/Common/SelectSingle';
import { GraphColoringScheme } from '../../../models/miscelanious';
import { mapLabelColoring, mapLableLayout } from '../../../utils/labels';
import { SelectorGraphColoring, SelectorGraphLayout } from '../../../utils/selectors';
interface GraphSidebarProps {
coloring: GraphColoringScheme
layout: LayoutTypes
setLayout: (newValue: LayoutTypes) => void
setColoring: (newValue: GraphColoringScheme) => void
}
function GraphSidebar({
coloring, setColoring,
layout, setLayout
} : GraphSidebarProps) {
return (
<div className='flex flex-col px-2 pb-2 mt-8 text-sm select-none h-fit'>
<div className='flex items-center w-full gap-1 text-sm'>
<SelectSingle
placeholder='Выберите цвет'
options={SelectorGraphColoring}
isSearchable={false}
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
/>
</div>
<SelectSingle
placeholder='Способ расположения'
className='w-full mt-1'
options={SelectorGraphLayout}
isSearchable={false}
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
/>
</div>);
}
export default GraphSidebar;

View File

@ -0,0 +1,76 @@
import ConceptTooltip from '../../../components/Common/ConceptTooltip'
import MiniButton from '../../../components/Common/MiniButton'
import HelpTermGraph from '../../../components/Help/HelpTermGraph'
import { ArrowsFocusIcon, DumpBinIcon, FilterIcon, HelpIcon, LetterAIcon, LetterALinesIcon, PlanetIcon, SmallPlusIcon } from '../../../components/Icons'
interface GraphToolbarProps {
editorMode: boolean
nothingSelected: boolean
is3D: boolean
orbit: boolean
noText: boolean
showParamsDialog: () => void
onCreate: () => void
onDelete: () => void
onResetViewpoint: () => void
toggleNoText: () => void
toggleOrbit: () => void
}
function GraphToolbar({
editorMode, nothingSelected, is3D,
noText, toggleNoText,
orbit, toggleOrbit,
showParamsDialog,
onCreate, onDelete, onResetViewpoint
} : GraphToolbarProps) {
return (
<div className='relative w-full z-pop'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<MiniButton
tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='text-primary' size={5} />}
onClick={showParamsDialog} />
<MiniButton
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!noText
? <LetterALinesIcon color='text-success' size={5} />
: <LetterAIcon color='text-primary' size={5} />
}
onClick={toggleNoText} />
<MiniButton
tooltip='Новая конституента'
icon={<SmallPlusIcon color={editorMode ? 'text-success' : ''} size={5} />}
disabled={!editorMode}
onClick={onCreate} />
<MiniButton
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={editorMode && !nothingSelected ? 'text-warning' : ''} size={5} />}
disabled={!editorMode || nothingSelected}
onClick={onDelete} />
<MiniButton
icon={<ArrowsFocusIcon color='text-primary' size={5} />}
tooltip='Восстановить камеру'
onClick={onResetViewpoint} />
<MiniButton
icon={<PlanetIcon color={!is3D ? '' : orbit ? 'text-success' : 'text-primary'} size={5} />}
tooltip='Анимация вращения'
disabled={!is3D}
onClick={toggleOrbit} />
<div className='px-1 py-1' id='items-graph-help'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#items-graph-help'>
<div className='text-sm max-w-[calc(100vw-20rem)] z-tooltip'>
<HelpTermGraph />
</div>
</ConceptTooltip>
</div>
</div>);
}
export default GraphToolbar;

View File

@ -0,0 +1,139 @@
import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
import { GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, Sphere, useSelection } from 'reagraph';
import { useConceptTheme } from '../../../context/ThemeContext';
import { graphDarkT, graphLightT } from '../../../utils/color';
import { resources } from '../../../utils/constants';
interface TermGraphProps {
nodes: GraphNode[]
edges: GraphEdge[]
layout: LayoutTypes
is3D: boolean
orbit: boolean
setSelected: (selected: number[]) => void
setHoverID: (newID: number | undefined) => void
onEdit: (cstID: number) => void
onDeselect: () => void
toggleResetView: boolean
toggleResetSelection: boolean
}
const TREE_SIZE_MILESTONE = 50;
function TermGraph({
nodes, edges,
layout, is3D, orbit,
toggleResetView, toggleResetSelection,
setHoverID, onEdit,
setSelected, onDeselect
} : TermGraphProps) {
const { noNavigation, darkMode } = useConceptTheme();
const graphRef = useRef<GraphCanvasRef | null>(null);
const {
selections, actives,
onNodeClick,
clearSelections,
onCanvasClick,
onNodePointerOver,
onNodePointerOut
} = useSelection({
ref: graphRef,
nodes,
edges,
type: 'multi', // 'single' | 'multi' | 'multiModifier'
pathSelectionType: 'out',
pathHoverType: 'all',
focusOnSelect: false
});
const handleHoverIn = useCallback(
(node: GraphNode) => {
setHoverID(Number(node.id));
if (onNodePointerOver) onNodePointerOver(node);
}, [onNodePointerOver, setHoverID]);
const handleHoverOut = useCallback(
(node: GraphNode) => {
setHoverID(undefined);
if (onNodePointerOut) onNodePointerOut(node);
}, [onNodePointerOut, setHoverID]);
const handleNodeClick = useCallback(
(node: GraphNode) => {
if (selections.includes(node.id)) {
onEdit(Number(node.id));
return;
}
if (onNodeClick) onNodeClick(node);
}, [onNodeClick, selections, onEdit]);
const handleCanvasClick = useCallback(
(event: MouseEvent) => {
onDeselect();
if (onCanvasClick) onCanvasClick(event);
}, [onCanvasClick, onDeselect]);
useLayoutEffect(
() => {
graphRef.current?.resetControls(true);
graphRef.current?.centerGraph();
}, [toggleResetView]);
useLayoutEffect(
() => {
clearSelections();
}, [toggleResetSelection, clearSelections]);
useLayoutEffect(
() => {
setSelected(selections.map(id => Number(id)));
}, [selections, setSelected]);
const canvasWidth = useMemo(
() => {
return 'calc(100vw - 1.1rem)';
}, []);
const canvasHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 9.8rem - 4px)'
: 'calc(100vh - 3rem - 4px)';
}, [noNavigation]);
return (
<div className='w-full h-full overflow-auto outline-none'>
<div className='relative' style={{width: canvasWidth, height: canvasHeight}}>
<GraphCanvas
draggable
ref={graphRef}
nodes={nodes}
edges={edges}
layoutType={layout}
selections={selections}
actives={actives}
onNodeClick={handleNodeClick}
onCanvasClick={handleCanvasClick}
onNodePointerOver={handleHoverIn}
onNodePointerOut={handleHoverOut}
cameraMode={ orbit ? 'orbit' : is3D ? 'rotate' : 'pan'}
layoutOverrides={
layout.includes('tree') ? { nodeLevelRatio: nodes.length < TREE_SIZE_MILESTONE ? 3 : 1 }
: undefined
}
labelFontUrl={resources.graph_font}
theme={darkMode ? graphDarkT : graphLightT}
renderNode={({ node, ...rest }) => (
<Sphere {...rest} node={node} />
)}
/>
</div>
</div>);
}
export default TermGraph;

View File

@ -0,0 +1,74 @@
import { useCallback, useMemo } from 'react';
import ConstituentaTooltip from '../../../components/Help/ConstituentaTooltip';
import { useConceptTheme } from '../../../context/ThemeContext';
import { GraphColoringScheme } from '../../../models/miscelanious';
import { IRSForm } from '../../../models/rsform';
import { colorbgGraphNode } from '../../../utils/color';
import { prefixes } from '../../../utils/constants';
interface ViewHiddenProps {
items: number[]
selected: number[]
schema: IRSForm
coloringScheme: GraphColoringScheme
toggleSelection: (cstID: number) => void
onEdit: (cstID: number) => void
}
function ViewHidden({
items,
selected, toggleSelection,
schema, coloringScheme,
onEdit
} : ViewHiddenProps) {
const { colors, noNavigation } = useConceptTheme();
const dismissedHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 28rem - 4px)'
: 'calc(100vh - 22.2rem - 4px)';
}, [noNavigation]);
const dismissedStyle = useCallback(
(cstID: number) => {
return selected.includes(cstID) ? {outlineWidth: '2px', outlineStyle: 'solid'}: {};
}, [selected]);
if (items.length <= 0) {
return null;
}
return (
<div className='flex flex-col text-sm ml-2 border clr-app max-w-[12.5rem] min-w-[12.5rem]'>
<p className='pt-2 text-center'><b>Скрытые конституенты</b></p>
<div className='flex flex-wrap justify-center gap-2 py-2 overflow-y-auto' style={{ maxHeight: dismissedHeight }}>
{items.map(
(cstID) => {
const cst = schema.items.find(cst => cst.id === cstID)!;
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
return (
<div key={`wrap-${id}`}>
<div key={id} id={id}
className='w-fit min-w-[3rem] rounded-md text-center cursor-pointer select-none'
style={{
backgroundColor: colorbgGraphNode(cst, adjustedColoring, colors),
...dismissedStyle(cstID)
}}
onClick={() => toggleSelection(cstID)}
onDoubleClick={() => onEdit(cstID)}
>
{cst.alias}
</div>
<ConstituentaTooltip
data={cst}
anchor={`#${id}`} />
</div>);
})}
</div>
</div>);
}
export default ViewHidden;

View File

@ -0,0 +1 @@
export { default } from './EditorTermGraph';

View File

@ -0,0 +1,57 @@
import { useLayoutEffect, useMemo, useState } from 'react';
import { GraphFilterParams } from '../../../models/miscelanious';
import { CstType, IRSForm } from '../../../models/rsform';
import { Graph } from '../../../utils/Graph';
function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, toggleUpdate: boolean) {
const [ filtered, setFiltered ] = useState<Graph>(new Graph());
const allowedTypes: CstType[] = useMemo(
() => {
const result: CstType[] = [];
if (params.allowBase) result.push(CstType.BASE);
if (params.allowStruct) result.push(CstType.STRUCTURED);
if (params.allowTerm) result.push(CstType.TERM);
if (params.allowAxiom) result.push(CstType.AXIOM);
if (params.allowFunction) result.push(CstType.FUNCTION);
if (params.allowPredicate) result.push(CstType.PREDICATE);
if (params.allowConstant) result.push(CstType.CONSTANT);
if (params.allowTheorem) result.push(CstType.THEOREM);
return result;
}, [params]);
useLayoutEffect(
() => {
if (!schema) {
setFiltered(new Graph());
return;
}
const graph = schema.graph.clone();
if (params.noHermits) {
graph.removeIsolated();
}
if (params.noTransitive) {
graph.transitiveReduction();
}
if (params.noTemplates) {
schema.items.forEach(cst => {
if (cst.is_template) {
graph.foldNode(cst.id);
}
});
}
if (allowedTypes.length < Object.values(CstType).length) {
schema.items.forEach(cst => {
if (!allowedTypes.includes(cst.cst_type)) {
graph.foldNode(cst.id);
}
});
}
setFiltered(graph);
}, [schema, params, allowedTypes, toggleUpdate]);
return filtered;
}
export default useGraphFilter;

View File

@ -27,10 +27,10 @@ import { SyntaxTree } from '../../models/rslang';
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { createAliasFor } from '../../utils/misc';
import EditorConstituenta from './EditorConstituenta';
import EditorItems from './EditorItems';
import EditorRSForm from './EditorRSForm';
import EditorRSList from './EditorRSList';
import EditorTermGraph from './EditorTermGraph';
import RSTabsMenu from './elements/RSTabsMenu';
import RSTabsMenu from './RSTabsMenu';
export enum RSTabID {
CARD = 0,
@ -425,7 +425,7 @@ function RSTabs() {
</TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
<EditorItems
<EditorRSList
onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst}

View File

@ -1,20 +1,21 @@
import { useNavigate } from 'react-router-dom';
import Button from '../../../components/Common/Button';
import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton';
import DropdownCheckbox from '../../../components/Common/DropdownCheckbox';
import Button from '../../components/Common/Button';
import Dropdown from '../../components/Common/Dropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import DropdownCheckbox from '../../components/Common/DropdownCheckbox';
import {
CloneIcon, DownloadIcon, DumpBinIcon, EditIcon, MenuIcon, NotSubscribedIcon,
OwnerIcon, ShareIcon, SmallPlusIcon, SubscribedIcon, UploadIcon
} from '../../../components/Icons';
import { useAuth } from '../../../context/AuthContext';
import { useRSForm } from '../../../context/RSFormContext';
import useDropdown from '../../../hooks/useDropdown';
} from '../../components/Icons';
import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown';
interface RSTabsMenuProps {
showUploadDialog: () => void
showCloneDialog: () => void
onDestroy: () => void
onClaim: () => void
onShare: () => void

View File

@ -3,7 +3,8 @@
*/
import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language'
import { CstClass, ExpressionStatus } from '../models/rsform'
import { GraphColoringScheme } from '../models/miscelanious'
import { CstClass, ExpressionStatus, IConstituenta } from '../models/rsform'
import { ISyntaxTreeNode, TokenID } from '../models/rslang'
@ -454,3 +455,16 @@ export function colorfgGrammeme(gram: GramData, colors: IColorTheme): string {
return colors.fgPurple;
}
}
/**
* Determines graph color for {@link IConstituenta}.
*/
export function colorbgGraphNode(cst: IConstituenta, coloringScheme: GraphColoringScheme, colors: IColorTheme): string {
if (coloringScheme === 'type') {
return colorbgCstClass(cst.cst_class, colors);
}
if (coloringScheme === 'status') {
return colorbgCstStatus(cst.status, colors);
}
return '';
}

View File

@ -5,8 +5,8 @@ import { LayoutTypes } from 'reagraph';
import { type GramData, Grammeme, ReferenceType } from '../models/language';
import { grammemeCompare } from '../models/languageAPI';
import { GraphColoringScheme } from '../models/miscelanious';
import { CstType } from '../models/rsform';
import { ColoringScheme } from '../pages/RSFormPage/EditorTermGraph';
import { labelGrammeme, labelReferenceType } from './labels';
import { labelCstType } from './labels';
@ -30,9 +30,9 @@ export const SelectorGraphLayout: { value: LayoutTypes, label: string }[] = [
];
/**
* Represents options for {@link ColoringScheme} selector.
* Represents options for {@link GraphColoringScheme} selector.
*/
export const SelectorGraphColoring: { value: ColoringScheme, label: string }[] = [
export const SelectorGraphColoring: { value: GraphColoringScheme, label: string }[] = [
{ value: 'none', label: 'Цвет: моно' },
{ value: 'status', label: 'Цвет: статус' },
{ value: 'type', label: 'Цвет: класс' },