From 50de0176f6827e3066c72b9a6f31a763c4659693 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:27:23 +0300 Subject: [PATCH] Refactor EditorTermGraph decouple responsibilities --- .../frontend/src/dialogs/DlgGraphOptions.tsx | 105 ---- .../frontend/src/dialogs/DlgGraphParams.tsx | 104 ++++ rsconcept/frontend/src/models/miscelanious.ts | 9 +- .../EditorConstituenta.tsx | 26 +- .../EditorRSExpression.tsx | 32 +- .../ParsingResult.tsx | 0 .../RSEditControls.tsx} | 0 .../RSLocalButton.tsx | 0 .../RSTokenButton.tsx | 0 .../StatusBar.tsx | 0 .../ViewSideConstituents.tsx | 0 .../RSFormPage/EditorConstituenta/index.tsx | 1 + .../{ => EditorRSForm}/EditorRSForm.tsx | 30 +- .../RSFormStats.tsx | 0 .../pages/RSFormPage/EditorRSForm/index.tsx | 1 + .../EditorRSList.tsx} | 27 +- .../RSListToolbar.tsx} | 37 +- .../pages/RSFormPage/EditorRSList/index.tsx | 1 + .../src/pages/RSFormPage/EditorTermGraph.tsx | 510 ------------------ .../EditorTermGraph/EditorTermGraph.tsx | 272 ++++++++++ .../EditorTermGraph/GraphSidebar.tsx | 42 ++ .../EditorTermGraph/GraphToolbar.tsx | 76 +++ .../RSFormPage/EditorTermGraph/TermGraph.tsx | 139 +++++ .../RSFormPage/EditorTermGraph/ViewHidden.tsx | 74 +++ .../RSFormPage/EditorTermGraph/index.tsx | 1 + .../EditorTermGraph/useGraphFilter.ts | 57 ++ .../frontend/src/pages/RSFormPage/RSTabs.tsx | 6 +- .../RSFormPage/{elements => }/RSTabsMenu.tsx | 17 +- rsconcept/frontend/src/utils/color.ts | 16 +- rsconcept/frontend/src/utils/selectors.ts | 6 +- 30 files changed, 881 insertions(+), 708 deletions(-) delete mode 100644 rsconcept/frontend/src/dialogs/DlgGraphOptions.tsx create mode 100644 rsconcept/frontend/src/dialogs/DlgGraphParams.tsx rename rsconcept/frontend/src/pages/RSFormPage/{ => EditorConstituenta}/EditorConstituenta.tsx (92%) rename rsconcept/frontend/src/pages/RSFormPage/{ => EditorConstituenta}/EditorRSExpression.tsx (84%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorConstituenta}/ParsingResult.tsx (100%) rename rsconcept/frontend/src/pages/RSFormPage/{elements/RSEditorControls.tsx => EditorConstituenta/RSEditControls.tsx} (100%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorConstituenta}/RSLocalButton.tsx (100%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorConstituenta}/RSTokenButton.tsx (100%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorConstituenta}/StatusBar.tsx (100%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorConstituenta}/ViewSideConstituents.tsx (100%) create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/index.tsx rename rsconcept/frontend/src/pages/RSFormPage/{ => EditorRSForm}/EditorRSForm.tsx (89%) rename rsconcept/frontend/src/pages/RSFormPage/{elements => EditorRSForm}/RSFormStats.tsx (100%) create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/index.tsx rename rsconcept/frontend/src/pages/RSFormPage/{EditorItems.tsx => EditorRSList/EditorRSList.tsx} (93%) rename rsconcept/frontend/src/pages/RSFormPage/{elements/RSItemsMenu.tsx => EditorRSList/RSListToolbar.tsx} (77%) create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorRSList/index.tsx delete mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphSidebar.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/index.tsx create mode 100644 rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts rename rsconcept/frontend/src/pages/RSFormPage/{elements => }/RSTabsMenu.tsx (92%) diff --git a/rsconcept/frontend/src/dialogs/DlgGraphOptions.tsx b/rsconcept/frontend/src/dialogs/DlgGraphOptions.tsx deleted file mode 100644 index a864c41e..00000000 --- a/rsconcept/frontend/src/dialogs/DlgGraphOptions.tsx +++ /dev/null @@ -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 { - initial: GraphEditorParams - onConfirm: (params: GraphEditorParams) => void -} - -function DlgGraphOptions({ hideWindow, initial, onConfirm } : DlgGraphOptionsProps) { - const [params, updateParams] = usePartialUpdate(initial); - - const handleSubmit = () => { - hideWindow(); - onConfirm(params); - }; - - return ( - -
-
-

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

- updateParams({noTerms: value})} - /> - updateParams({ noHermits: value})} - /> - updateParams({ noTemplates: value})} - /> - updateParams({ noTransitive: value})} - /> -
-
-

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

- updateParams({ allowBase: value})} - /> - updateParams({ allowStruct: value})} - /> - updateParams({ allowTerm: value})} - /> - updateParams({ allowAxiom: value})} - /> - updateParams({ allowFunction: value})} - /> - updateParams({ allowPredicate: value})} - /> - updateParams({ allowConstant: value})} - /> - updateParams({ allowTheorem: value})} - /> -
-
-
- ); -} - -export default DlgGraphOptions; diff --git a/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx new file mode 100644 index 00000000..e3fa86b1 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx @@ -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 { + initial: GraphFilterParams + onConfirm: (params: GraphFilterParams) => void +} + +function DlgGraphParams({ hideWindow, initial, onConfirm } : DlgGraphParamsProps) { + const [params, updateParams] = usePartialUpdate(initial); + + const handleSubmit = () => { + hideWindow(); + onConfirm(params); + }; + + return ( + +
+
+

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

+ updateParams({noText: value})} + /> + updateParams({ noHermits: value})} + /> + updateParams({ noTemplates: value})} + /> + updateParams({ noTransitive: value})} + /> +
+
+

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

+ updateParams({ allowBase: value})} + /> + updateParams({ allowStruct: value})} + /> + updateParams({ allowTerm: value})} + /> + updateParams({ allowAxiom: value})} + /> + updateParams({ allowFunction: value})} + /> + updateParams({ allowPredicate: value})} + /> + updateParams({ allowConstant: value})} + /> + updateParams({ allowTheorem: value})} + /> +
+
+
); +} + +export default DlgGraphParams; diff --git a/rsconcept/frontend/src/models/miscelanious.ts b/rsconcept/frontend/src/models/miscelanious.ts index 0fd08b09..8da787c2 100644 --- a/rsconcept/frontend/src/models/miscelanious.ts +++ b/rsconcept/frontend/src/models/miscelanious.ts @@ -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 diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx similarity index 92% rename from rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx index 9f059a2f..aa3d2b2a 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorConstituenta.tsx @@ -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'; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorRSExpression.tsx similarity index 84% rename from rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorRSExpression.tsx index b04ee6a9..45894061 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSExpression.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/EditorRSExpression.tsx @@ -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 diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ParsingResult.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/ParsingResult.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/ParsingResult.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/ParsingResult.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/RSEditorControls.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSEditControls.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/RSEditorControls.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSEditControls.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/RSLocalButton.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSLocalButton.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/RSLocalButton.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSLocalButton.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/RSTokenButton.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSTokenButton.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/RSTokenButton.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/RSTokenButton.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/StatusBar.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/StatusBar.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/StatusBar.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/ViewSideConstituents.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/ViewSideConstituents.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/ViewSideConstituents.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/index.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/index.tsx new file mode 100644 index 00000000..23b4a220 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta/index.tsx @@ -0,0 +1 @@ +export { default } from './EditorConstituenta'; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/EditorRSForm.tsx similarity index 89% rename from rsconcept/frontend/src/pages/RSFormPage/EditorRSForm.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/EditorRSForm.tsx index 9fbb23af..5f6bb29b 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/EditorRSForm.tsx @@ -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 diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/RSFormStats.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/RSFormStats.tsx similarity index 100% rename from rsconcept/frontend/src/pages/RSFormPage/elements/RSFormStats.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/RSFormStats.tsx diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/index.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/index.tsx new file mode 100644 index 00000000..7a05933c --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSForm/index.tsx @@ -0,0 +1 @@ +export { default } from './EditorRSForm'; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx similarity index 93% rename from rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx index 5abfc90f..9f055a3d 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorItems.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/EditorRSList.tsx @@ -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(); -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} ); } -export default EditorItems; +export default EditorRSList; diff --git a/rsconcept/frontend/src/pages/RSFormPage/elements/RSItemsMenu.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/RSListToolbar.tsx similarity index 77% rename from rsconcept/frontend/src/pages/RSFormPage/elements/RSItemsMenu.tsx rename to rsconcept/frontend/src/pages/RSFormPage/EditorRSList/RSListToolbar.tsx index f5bcc22c..06227140 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/elements/RSItemsMenu.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/RSListToolbar.tsx @@ -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 (
} - disabled={!isEditable || nothingSelected} + disabled={!editorMode || nothingSelected} onClick={onMoveUp} /> } - disabled={!isEditable || nothingSelected} + disabled={!editorMode || nothingSelected} onClick={onMoveDown} /> } - disabled={!isEditable || nothingSelected} + icon={} + disabled={!editorMode || nothingSelected} onClick={onDelete} /> } - disabled={!isEditable || selected.length !== 1} + icon={} + disabled={!editorMode || selectedCount !== 1} onClick={onClone} /> } - disabled={!isEditable} + icon={} + disabled={!editorMode} onClick={() => onCreate()} />
} - disabled={!isEditable} + icon={} + disabled={!editorMode} onClick={insertMenu.toggle} /> { insertMenu.isActive && @@ -92,15 +91,15 @@ function RSItemsMenu({ } - disabled={!isEditable} + icon={} + disabled={!editorMode} onClick={onTemplates} /> } - disabled={!isEditable} + icon={} + disabled={!editorMode} onClick={onReindex} /> diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/index.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/index.tsx new file mode 100644 index 00000000..09c861f8 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorRSList/index.tsx @@ -0,0 +1 @@ +export { default } from './EditorRSList'; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx deleted file mode 100644 index a82983d5..00000000 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph.tsx +++ /dev/null @@ -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('graph_layout', 'treeTd2d'); - const [ coloringScheme, setColoringScheme ] = useLocalStorage('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(new Graph()); - const [ dismissed, setDismissed ] = useState([]); - const [ selectedDismissed, setSelectedDismissed ] = useState([]); - const graphRef = useRef(null); - const [showOptions, setShowOptions] = useState(false); - const [toggleUpdate, setToggleUpdate] = useState(false); - - const [hoverID, setHoverID] = useState(undefined); - const hoverCst = useMemo( - () => { - return schema?.items.find(cst => 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) { - 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 && - setShowOptions(false)} - initial={getOptions()} - onConfirm={handleChangeOptions} - />} - - { allSelected.length > 0 && -
-
- Выбор {allSelected.length} из {schema?.stats?.count_all ?? 0} -
-
} - -
-
- } - onClick={() => setShowOptions(true)} - /> - : } - onClick={() => setNoTerms(prev => !prev)} - /> - } - disabled={!isEditable} - onClick={handleCreateCst} - /> - } - disabled={!isEditable || nothingSelected} - onClick={handleDeleteCst} - /> - } - tooltip='Восстановить камеру' - onClick={handleResetViewpoint} - /> - } - tooltip='Анимация вращения' - disabled={!is3D} - onClick={() => setOrbit(prev => !prev) } - /> -
- -
- -
- -
-
-
-
- - {hoverCst && -
- -
} - -
-
-
-
- setColoringScheme(data?.value ?? SelectorGraphColoring[0].value)} - /> -
- handleChangeLayout(data?.value ?? SelectorGraphLayout[0].value)} - /> -
- {dismissed.length > 0 && -
-

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

-
- {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 (
-
toggleDismissed(cstID)} - onDoubleClick={() => onOpenEdit(cstID)} - > - {cst.alias} -
- -
); - })} -
-
} -
-
- -
-
- ( - - )} - /> -
-
); -} - - -export default EditorTermGraph; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx new file mode 100644 index 00000000..5f54a39d --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx @@ -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('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([]); + const [hidden, setHidden] = useState([]); + const [selectedHidden, setSelectedHidden] = useState([]); + const selected: number[] = useMemo( + () => { + return [...selectedHidden, ...selectedGraph]; + }, [selectedHidden, selectedGraph]); + const nothingSelected = useMemo(() => selected.length === 0, [selected]); + + const [layout, setLayout] = useLocalStorage('graph_layout', 'treeTd2d'); + const is3D = useMemo(() => layout.includes('3d'), [layout]); + const [coloringScheme, setColoringScheme] = useLocalStorage('graph_coloring', 'type'); + const [orbit, setOrbit] = useState(false); + + const [hoverID, setHoverID] = useState(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) { + // Hotkeys implementation + if (!isEditable) { + return; + } + if (event.key === 'Delete') { + event.preventDefault(); + handleDeleteCst(); + } + } + + return ( +
+ {showParamsDialog ? + setShowParamsDialog(false)} + initial={filterParams} + onConfirm={handleChangeParams} + /> : null} + + {selected.length > 0 ? +
+
+ Выбор {selected.length} из {schema?.stats?.count_all ?? 0} +
+
: null} + + setShowParamsDialog(true)} + onCreate={handleCreateCst} + onDelete={handleDeleteCst} + onResetViewpoint={() => setToggleResetView(prev => !prev)} + + toggleOrbit={() => setOrbit(prev => !prev)} + toggleNoText={() => setFilterParams( + (prev) => ({ + ...prev, + noText: !prev.noText + }) + )} + /> + + {hoverCst ? +
+ +
: null} + +
+
+ + +
+
+ + setSelectedHidden([])} + + toggleResetView={toggleResetView} + toggleResetSelection={toggleResetSelection} + /> +
); +} + +export default EditorTermGraph; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphSidebar.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphSidebar.tsx new file mode 100644 index 00000000..66d5b531 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphSidebar.tsx @@ -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 ( +
+
+ setColoring(data?.value ?? SelectorGraphColoring[0].value)} + /> +
+ setLayout(data?.value ?? SelectorGraphLayout[0].value)} + /> +
); +} + +export default GraphSidebar; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx new file mode 100644 index 00000000..ee1d8dea --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/GraphToolbar.tsx @@ -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 ( +
+
+ } + onClick={showParamsDialog} /> + + : + } + onClick={toggleNoText} /> + } + disabled={!editorMode} + onClick={onCreate} /> + } + disabled={!editorMode || nothingSelected} + onClick={onDelete} /> + } + tooltip='Восстановить камеру' + onClick={onResetViewpoint} /> + } + tooltip='Анимация вращения' + disabled={!is3D} + onClick={toggleOrbit} /> +
+ +
+ +
+ +
+
+
+
); +} + +export default GraphToolbar; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx new file mode 100644 index 00000000..8db10d1c --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/TermGraph.tsx @@ -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(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 ( +
+
+ ( + + )} + /> +
+
); +} + +export default TermGraph; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx new file mode 100644 index 00000000..22df9d35 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx @@ -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 ( +
+

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

+
+ {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 ( +
+
toggleSelection(cstID)} + onDoubleClick={() => onEdit(cstID)} + > + {cst.alias} +
+ +
); + })} +
+
); +} + +export default ViewHidden; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/index.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/index.tsx new file mode 100644 index 00000000..990cd64d --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/index.tsx @@ -0,0 +1 @@ +export { default } from './EditorTermGraph'; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts new file mode 100644 index 00000000..fb2565f9 --- /dev/null +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/useGraphFilter.ts @@ -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(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; diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index d99edf81..55b9b585 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -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() { - void showCloneDialog: () => void + onDestroy: () => void onClaim: () => void onShare: () => void diff --git a/rsconcept/frontend/src/utils/color.ts b/rsconcept/frontend/src/utils/color.ts index 230a1ea5..a5be43a2 100644 --- a/rsconcept/frontend/src/utils/color.ts +++ b/rsconcept/frontend/src/utils/color.ts @@ -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 ''; +} \ No newline at end of file diff --git a/rsconcept/frontend/src/utils/selectors.ts b/rsconcept/frontend/src/utils/selectors.ts index 8d749f9d..134c6838 100644 --- a/rsconcept/frontend/src/utils/selectors.ts +++ b/rsconcept/frontend/src/utils/selectors.ts @@ -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: 'Цвет: класс' },