mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactor EditorTermGraph
decouple responsibilities
This commit is contained in:
parent
b804586394
commit
50de0176f6
|
@ -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;
|
104
rsconcept/frontend/src/dialogs/DlgGraphParams.tsx
Normal file
104
rsconcept/frontend/src/dialogs/DlgGraphParams.tsx
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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';
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorConstituenta';
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorRSForm';
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorRSList';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './EditorTermGraph';
|
|
@ -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;
|
|
@ -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}
|
||||
|
|
|
@ -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
|
|
@ -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 '';
|
||||
}
|
|
@ -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: 'Цвет: класс' },
|
||||
|
|
Loading…
Reference in New Issue
Block a user