diff --git a/README.md b/README.md index 51d4dd10..eac3ef10 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ This readme file is used mostly to document project dependencies - react-error-boundary - react-pdf - react-tooltip + - reactflow - js-file-download - use-debounce - framer-motion @@ -54,6 +55,7 @@ This readme file is used mostly to document project dependencies - autoprefixer - eslint-plugin-simple-import-sort - eslint-plugin-tsdoc + - vite - jest - ts-jest - @types/jest diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index fe010453..0b02f26a 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -2,7 +2,7 @@ from apps.rsform.serializers import LibraryItemSerializer -from .basics import OperationPositionSerializer, PositionsSerializer +from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer from .data_access import ( ArgumentSerializer, OperationCreateSerializer, diff --git a/rsconcept/backend/apps/oss/serializers/basics.py b/rsconcept/backend/apps/oss/serializers/basics.py index d597ca5a..f0ea8d3b 100644 --- a/rsconcept/backend/apps/oss/serializers/basics.py +++ b/rsconcept/backend/apps/oss/serializers/basics.py @@ -14,3 +14,15 @@ class PositionsSerializer(serializers.Serializer): positions = serializers.ListField( child=OperationPositionSerializer() ) + + +class SubstitutionExSerializer(serializers.Serializer): + ''' Serializer: Substitution extended data. ''' + operation = serializers.IntegerField() + original = serializers.IntegerField() + substitution = serializers.IntegerField() + transfer_term = serializers.BooleanField() + original_alias = serializers.CharField() + original_term = serializers.CharField() + substitution_alias = serializers.CharField() + substitution_term = serializers.CharField() diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index eb161ff6..c2d83d01 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -10,7 +10,7 @@ from apps.rsform.serializers import LibraryItemDetailsSerializer from shared import messages as msg from ..models import Argument, Operation, OperationSchema, OperationType -from .basics import OperationPositionSerializer +from .basics import OperationPositionSerializer, SubstitutionExSerializer class OperationSerializer(serializers.ModelSerializer): @@ -75,9 +75,12 @@ class OperationSchemaSerializer(serializers.ModelSerializer): items = serializers.ListField( child=OperationSerializer() ) - graph = serializers.ListField( + arguments = serializers.ListField( child=ArgumentSerializer() ) + substitutions = serializers.ListField( + child=SubstitutionExSerializer() + ) class Meta: ''' serializer metadata. ''' @@ -90,15 +93,15 @@ class OperationSchemaSerializer(serializers.ModelSerializer): result['items'] = [] for operation in oss.operations(): result['items'].append(OperationSerializer(operation).data) - result['graph'] = [] + result['arguments'] = [] for argument in oss.arguments(): - result['graph'].append(ArgumentSerializer(argument).data) + result['arguments'].append(ArgumentSerializer(argument).data) result['substitutions'] = [] for substitution in oss.substitutions().values( 'operation', 'original', - 'transfer_term', 'substitution', + 'transfer_term', original_alias=F('original__alias'), original_term=F('original__term_resolved'), substitution_alias=F('substitution__alias'), diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 1247148a..ba9b7052 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -77,12 +77,12 @@ class TestOssViewset(EndpointTester): self.assertEqual(sub['substitution_alias'], self.ks2x1.alias) self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved) - graph = response.data['graph'] - self.assertEqual(len(graph), 2) - self.assertEqual(graph[0]['operation'], self.operation3.pk) - self.assertEqual(graph[0]['argument'], self.operation1.pk) - self.assertEqual(graph[1]['operation'], self.operation3.pk) - self.assertEqual(graph[1]['argument'], self.operation2.pk) + arguments = response.data['arguments'] + self.assertEqual(len(arguments), 2) + self.assertEqual(arguments[0]['operation'], self.operation3.pk) + self.assertEqual(arguments[0]['argument'], self.operation1.pk) + self.assertEqual(arguments[1]['operation'], self.operation3.pk) + self.assertEqual(arguments[1]['argument'], self.operation2.pk) self.executeOK(item=self.unowned_id) self.executeForbidden(item=self.private_id) diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts index 01226581..c65ee65e 100644 --- a/rsconcept/frontend/src/app/backendAPI.ts +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -7,17 +7,14 @@ import { toast } from 'react-toastify'; import { type ErrorData } from '@/components/info/InfoError'; import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; -import { - AccessPolicy, - ILibraryItem, - ILibraryUpdateData, - ITargetAccessPolicy, - ITargetLocation, - IVersionData, - LibraryItemType -} from '@/models/library'; +import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library'; -import { IOperationSchemaData } from '@/models/oss'; +import { + ICstSubstituteData, + IOperationCreateData, + IOperationCreatedResponse, + IOperationSchemaData +} from '@/models/oss'; import { IConstituentaList, IConstituentaMeta, @@ -25,7 +22,6 @@ import { ICstCreatedResponse, ICstMovetoData, ICstRenameData, - ICstSubstituteData, ICstUpdateData, IInlineSynthesisData, IProduceStructureResponse, @@ -233,30 +229,6 @@ export function postCloneLibraryItem(target: string, request: FrontExchange) { - request.setLoading!(false); - request.onSuccess({ - id: Number(target), - comment: '123', - alias: 'oss1', - access_policy: AccessPolicy.PUBLIC, - editors: [], - owner: 1, - item_type: LibraryItemType.OSS, - location: '/U', - read_only: false, - subscribers: [], - time_create: '0', - time_update: '0', - title: 'TestOss', - visible: false - }); - // AxiosGet({ - // endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS - // request: request - // }); -} - export function getRSFormDetails(target: string, version: string, request: FrontPull) { if (!version) { AxiosGet({ @@ -357,7 +329,7 @@ export function getTRSFile(target: string, version: string, request: FrontPull) { +export function postCreateConstituenta(schema: string, request: FrontExchange) { AxiosPost({ endpoint: `/api/rsforms/${schema}/cst-create`, request: request @@ -445,6 +417,23 @@ export function patchInlineSynthesis(request: FrontExchange) { + AxiosGet({ + endpoint: `/api/oss/${target}/details`, + request: request + }); +} + +export function postCreateOperation( + schema: string, + request: FrontExchange +) { + AxiosPost({ + endpoint: `/api/oss/${schema}/create-operation`, + request: request + }); +} + export function postInflectText(request: FrontExchange) { AxiosPost({ endpoint: `/api/cctext/inflect`, diff --git a/rsconcept/frontend/src/components/select/PickSubstitutions.tsx b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx index ada311e1..88e6eadc 100644 --- a/rsconcept/frontend/src/components/select/PickSubstitutions.tsx +++ b/rsconcept/frontend/src/components/select/PickSubstitutions.tsx @@ -8,7 +8,7 @@ import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import Label from '@/components/ui/Label'; import MiniButton from '@/components/ui/MiniButton'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; -import { IConstituenta, IRSForm, ISubstitution } from '@/models/rsform'; +import { IConstituenta, IRSForm, ISingleSubstitution } from '@/models/rsform'; import { describeConstituenta } from '@/utils/labels'; import { @@ -34,11 +34,11 @@ interface PickSubstitutionsProps { filter1?: (cst: IConstituenta) => boolean; filter2?: (cst: IConstituenta) => boolean; - items: ISubstitution[]; - setItems: React.Dispatch>; + items: ISingleSubstitution[]; + setItems: React.Dispatch>; } -function SubstitutionIcon({ item }: { item: ISubstitution }) { +function SubstitutionIcon({ item }: { item: ISingleSubstitution }) { if (item.deleteRight) { if (item.takeLeftTerm) { return ; @@ -54,7 +54,7 @@ function SubstitutionIcon({ item }: { item: ISubstitution }) { } } -const columnHelper = createColumnHelper(); +const columnHelper = createColumnHelper(); function PickSubstitutions({ items, @@ -80,7 +80,7 @@ function PickSubstitutions({ if (!leftCst || !rightCst) { return; } - const newSubstitution: ISubstitution = { + const newSubstitution: ISingleSubstitution = { leftCst: leftCst, rightCst: rightCst, deleteRight: deleteRight, @@ -99,7 +99,7 @@ function PickSubstitutions({ const handleDeleteRow = useCallback( (row: number) => { setItems(prev => { - const newItems: ISubstitution[] = []; + const newItems: ISingleSubstitution[] = []; prev.forEach((item, index) => { if (index !== row) { newItems.push(item); diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index aa95ab43..07b6d83e 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -10,13 +10,14 @@ import { patchSetAccessPolicy, patchSetLocation, patchSetOwner, + postCreateOperation, postSubscribe } from '@/app/backendAPI'; import { type ErrorData } from '@/components/info/InfoError'; import useOssDetails from '@/hooks/useOssDetails'; import { AccessPolicy, ILibraryItem } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; -import { IOperationSchema } from '@/models/oss'; +import { IOperation, IOperationCreateData, IOperationSchema } from '@/models/oss'; import { UserID } from '@/models/user'; import { contextOutsideScope } from '@/utils/labels'; @@ -43,6 +44,8 @@ interface IOssContext { setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void; setLocation: (newLocation: string, callback?: () => void) => void; setEditors: (newEditors: UserID[], callback?: () => void) => void; + + createOperation: (data: IOperationCreateData, callback?: DataCallback) => void; } const OssContext = createContext(null); @@ -63,13 +66,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => { const library = useLibrary(); const { user } = useAuth(); const { - schema: schema, // prettier: split lines + schema, // prettier: split lines error: errorLoading, setSchema, loading - } = useOssDetails({ - target: itemID - }); + } = useOssDetails({ target: itemID }); const [processing, setProcessing] = useState(false); const [processingError, setProcessingError] = useState(undefined); @@ -249,6 +250,24 @@ export const OssState = ({ itemID, children }: OssStateProps) => { [itemID, schema] ); + const createOperation = useCallback( + (data: IOperationCreateData, callback?: DataCallback) => { + setProcessingError(undefined); + postCreateOperation(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: newData => { + setSchema(newData.oss); + library.localUpdateTimestamp(newData.oss.id); + if (callback) callback(newData.new_operation); + } + }); + }, + [itemID, library, setSchema] + ); + return ( { setOwner, setEditors, setAccessPolicy, - setLocation + setLocation, + + createOperation }} > {children} diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index a54e2394..211e4fbf 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -24,14 +24,15 @@ import { patchSubstituteConstituents, patchUploadTRS, patchVersion, + postCreateConstituenta, postCreateVersion, - postNewConstituenta, postSubscribe } from '@/app/backendAPI'; import { type ErrorData } from '@/components/info/InfoError'; import useRSFormDetails from '@/hooks/useRSFormDetails'; import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; +import { ICstSubstituteData } from '@/models/oss'; import { ConstituentaID, IConstituentaList, @@ -39,7 +40,6 @@ import { ICstCreateData, ICstMovetoData, ICstRenameData, - ICstSubstituteData, ICstUpdateData, IInlineSynthesisData, IRSForm, @@ -399,7 +399,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = const cstCreate = useCallback( (data: ICstCreateData, callback?: DataCallback) => { setProcessingError(undefined); - postNewConstituenta(itemID, { + postCreateConstituenta(itemID, { data: data, showError: true, setLoading: setProcessing, diff --git a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx index d0a0a300..731fb7e5 100644 --- a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx +++ b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx @@ -8,7 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal'; import TabLabel from '@/components/ui/TabLabel'; import useRSFormDetails from '@/hooks/useRSFormDetails'; import { LibraryItemID } from '@/models/library'; -import { IInlineSynthesisData, IRSForm, ISubstitution } from '@/models/rsform'; +import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform'; import TabConstituents from './TabConstituents'; import TabSchema from './TabSchema'; @@ -30,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli const [donorID, setDonorID] = useState(undefined); const [selected, setSelected] = useState([]); - const [substitutions, setSubstitutions] = useState([]); + const [substitutions, setSubstitutions] = useState([]); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); diff --git a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/TabSubstitutions.tsx b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/TabSubstitutions.tsx index 9057a106..5a5fa489 100644 --- a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/TabSubstitutions.tsx +++ b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/TabSubstitutions.tsx @@ -2,7 +2,7 @@ import { ErrorData } from '@/components/info/InfoError'; import DataLoader from '@/components/wrap/DataLoader'; -import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform'; +import { ConstituentaID, IRSForm, ISingleSubstitution } from '@/models/rsform'; import { prefixes } from '@/utils/constants'; import PickSubstitutions from '../../components/select/PickSubstitutions'; @@ -15,8 +15,8 @@ interface TabSubstitutionsProps { loading?: boolean; error?: ErrorData; - substitutions: ISubstitution[]; - setSubstitutions: React.Dispatch>; + substitutions: ISingleSubstitution[]; + setSubstitutions: React.Dispatch>; } function TabSubstitutions({ diff --git a/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx b/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx index 96585f03..7a7fe00f 100644 --- a/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx +++ b/rsconcept/frontend/src/dialogs/DlgSubstituteCst.tsx @@ -6,7 +6,8 @@ import { useMemo, useState } from 'react'; import PickSubstitutions from '@/components/select/PickSubstitutions'; import Modal, { ModalProps } from '@/components/ui/Modal'; import { useRSForm } from '@/context/RSFormContext'; -import { ICstSubstituteData, ISubstitution } from '@/models/rsform'; +import { ICstSubstituteData } from '@/models/oss'; +import { ISingleSubstitution } from '@/models/rsform'; import { prefixes } from '@/utils/constants'; interface DlgSubstituteCstProps extends Pick { @@ -16,7 +17,7 @@ interface DlgSubstituteCstProps extends Pick { function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) { const { schema } = useRSForm(); - const [substitutions, setSubstitutions] = useState([]); + const [substitutions, setSubstitutions] = useState([]); const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]); diff --git a/rsconcept/frontend/src/models/OssLoader.ts b/rsconcept/frontend/src/models/OssLoader.ts index 2e3d0a09..bf0f8ed2 100644 --- a/rsconcept/frontend/src/models/OssLoader.ts +++ b/rsconcept/frontend/src/models/OssLoader.ts @@ -2,22 +2,40 @@ * Module: OSS data loading and processing. */ -import { IOperationSchema, IOperationSchemaData } from './oss'; +import { Graph } from './Graph'; +import { IOperation, IOperationSchema, IOperationSchemaData, OperationID } from './oss'; /** * Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}. * */ export class OssLoader { - private schema: IOperationSchemaData; + private oss: IOperationSchemaData; + private graph: Graph = new Graph(); + private operationByID: Map = new Map(); constructor(input: IOperationSchemaData) { - this.schema = input; + this.oss = input; } produceOSS(): IOperationSchema { - const result = this.schema as IOperationSchema; - result.producedData = [1, 2, 3]; // TODO: put data processing here + const result = this.oss as IOperationSchema; + this.prepareLookups(); + this.createGraph(); + + result.operationByID = this.operationByID; + result.graph = this.graph; return result; } + + private prepareLookups() { + this.oss.items.forEach(operation => { + this.operationByID.set(operation.id, operation); + this.graph.addNode(operation.id); + }); + } + + private createGraph() { + this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation)); + } } diff --git a/rsconcept/frontend/src/models/miscellaneous.ts b/rsconcept/frontend/src/models/miscellaneous.ts index 7cda6856..062541c7 100644 --- a/rsconcept/frontend/src/models/miscellaneous.ts +++ b/rsconcept/frontend/src/models/miscellaneous.ts @@ -177,3 +177,11 @@ export interface GraphFilterParams { allowConstant: boolean; allowTheorem: boolean; } + +/** + * Represents XY Position. + */ +export interface Position2D { + x: number; + y: number; +} diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index 9c426f8b..5d3ef975 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -2,18 +2,101 @@ * Module: Schema of Synthesis Operations. */ -import { ILibraryItemData } from './library'; +import { Graph } from './Graph'; +import { ILibraryItemData, LibraryItemID } from './library'; +import { ConstituentaID } from './rsform'; /** - * Represents backend data for Schema of Synthesis Operations. + * Represents {@link IOperation} identifier type. + */ +export type OperationID = number; + +/** + * Represents {@link IOperation} type. + */ +export enum OperationType { + INPUT = 'input', + SYNTHESIS = 'synthesis' +} + +/** + * Represents Operation. + */ +export interface IOperation { + id: OperationID; + operation_type: OperationType; + oss: LibraryItemID; + + alias: string; + title: string; + comment: string; + position_x: number; + position_y: number; + + result: LibraryItemID; +} + +/** + * Represents {@link IOperation} data, used in creation process. + */ +export interface IOperationCreateData + extends Pick {} + +/** + * Represents {@link IOperation} Argument. + */ +export interface IArgument { + operation: OperationID; + argument: OperationID; +} + +/** + * Represents data, used in merging single {@link IConstituenta}. + */ +export interface ICstSubstitute { + original: ConstituentaID; + substitution: ConstituentaID; + transfer_term: boolean; +} + +/** + * Represents data, used in merging multiple {@link IConstituenta}. + */ +export interface ICstSubstituteData { + substitutions: ICstSubstitute[]; +} + +/** + * Represents {@link ICstSubstitute} extended data. + */ +export interface ICstSubstituteEx extends ICstSubstitute { + original_alias: string; + original_term: string; + substitution_alias: string; + substitution_term: string; +} + +/** + * Represents backend data for {@link IOperationSchema}. */ export interface IOperationSchemaData extends ILibraryItemData { - additional_data?: number[]; + items: IOperation[]; + arguments: IArgument[]; + substitutions: ICstSubstituteEx[]; } /** - * Represents Schema of Synthesis Operations. + * Represents OperationSchema. */ export interface IOperationSchema extends IOperationSchemaData { - producedData: number[]; // TODO: modify this to store calculated state on load + graph: Graph; + operationByID: Map; +} + +/** + * Represents data response when creating {@link IOperation}. + */ +export interface IOperationCreatedResponse { + new_operation: IOperation; + oss: IOperationSchemaData; } diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index 428e8326..9cada644 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -5,10 +5,11 @@ import { Graph } from '@/models/Graph'; import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library'; +import { ICstSubstitute } from './oss'; import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'; /** - * Represents Constituenta type. + * Represents {@link IConstituenta} type. */ export enum CstType { BASE = 'basic', @@ -21,7 +22,7 @@ export enum CstType { THEOREM = 'theorem' } -// CstType constant for category dividers in TemplateSchemas. TODO: create separate structure for templates +// CstType constant for category dividers in TemplateSchemas export const CATEGORY_CST_TYPE = CstType.THEOREM; /** @@ -30,7 +31,7 @@ export const CATEGORY_CST_TYPE = CstType.THEOREM; export type Position = number; /** - * Represents {@link Constituenta} identifier type. + * Represents {@link IConstituenta} identifier type. */ export type ConstituentaID = number; @@ -124,7 +125,7 @@ export interface IConstituentaList { } /** - * Represents constituenta data, used in creation process. + * Represents {@link IConstituenta} data, used in creation process. */ export interface ICstCreateData extends Pick< @@ -135,7 +136,7 @@ export interface ICstCreateData } /** - * Represents data, used in ordering constituents in a list. + * Represents data, used in ordering a list of {@link IConstituenta}. */ export interface ICstMovetoData extends IConstituentaList { move_to: Position; @@ -158,32 +159,6 @@ export interface ICstUpdateData */ export interface ICstRenameData extends ITargetCst, Pick {} -/** - * Represents data, used in merging single {@link IConstituenta}. - */ -export interface ICstSubstitute { - original: ConstituentaID; - substitution: ConstituentaID; - transfer_term: boolean; -} - -/** - * Represents data, used in merging multiple {@link IConstituenta}. - */ -export interface ICstSubstituteData { - substitutions: ICstSubstitute[]; -} - -/** - * Represents single substitution for synthesis table. - */ -export interface ISubstitution { - leftCst: IConstituenta; - rightCst: IConstituenta; - deleteRight: boolean; - takeLeftTerm: boolean; -} - /** * Represents data response when creating {@link IConstituenta}. */ @@ -265,6 +240,16 @@ export interface IVersionCreatedResponse { schema: IRSFormData; } +/** + * Represents single substitution for synthesis table. + */ +export interface ISingleSubstitution { + leftCst: IConstituenta; + rightCst: IConstituenta; + deleteRight: boolean; + takeLeftTerm: boolean; +} + /** * Represents input data for inline synthesis. */ diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx index 8d17b729..5d38ce39 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx @@ -2,19 +2,12 @@ import { ReactFlowProvider } from 'reactflow'; -import AnimateFade from '@/components/wrap/AnimateFade'; - -import { useOssEdit } from '../OssEditContext'; import OssFlow from './OssFlow'; function EditorOssGraph() { - const controller = useOssEdit(); - return ( - - - + ); } diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index a3f7fe3f..3886f6d1 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -1,49 +1,120 @@ 'use client'; -import { useMemo } from 'react'; -import { NodeTypes, ProOptions, ReactFlow } from 'reactflow'; +import { useCallback, useMemo } from 'react'; +import { + Edge, + EdgeChange, + Node, + NodeChange, + NodeTypes, + ProOptions, + ReactFlow, + useEdgesState, + useNodesState, + useViewport +} from 'reactflow'; +import Overlay from '@/components/ui/Overlay'; +import AnimateFade from '@/components/wrap/AnimateFade'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useOSS } from '@/context/OssContext'; -import { IOssEditContext } from '../OssEditContext'; +import { useOssEdit } from '../OssEditContext'; import InputNode from './InputNode'; import OperationNode from './OperationNode'; +import ToolbarOssGraph from './ToolbarOssGraph'; -const OssNodeTypes: NodeTypes = { - synthesis: OperationNode, - input: InputNode -}; - -interface OssFlowProps { - controller: IOssEditContext; -} - -function OssFlow({ controller }: OssFlowProps) { +function OssFlow() { const { calculateHeight } = useConceptOptions(); const model = useOSS(); + const controller = useOssEdit(); + const viewport = useViewport(); - console.log(model.loading); console.log(controller.isMutable); - const initialNodes = [ - { id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'input' }, - { id: '2', position: { x: 0, y: 100 }, data: { label: '2' }, type: 'synthesis' } - ]; - const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; + const initialNodes: Node[] = useMemo( + () => + !model.schema + ? [] + : model.schema.items.map(operation => ({ + id: String(operation.id), + data: { label: operation.alias }, + position: { x: operation.position_x, y: operation.position_y }, + type: operation.operation_type.toString() + })), + [model.schema] + ); + // const initialNodes = [ + // { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, + // { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } } + // ]; + + const initialEdges: Edge[] = useMemo( + () => + !model.schema + ? [] + : model.schema.arguments.map((argument, index) => ({ + id: String(index), + source: String(argument.argument), + target: String(argument.operation) + })), + [model.schema] + ); + // const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; + + const [nodes, onNodesChange] = useNodesState(initialNodes); + const [edges, onEdgesChange] = useEdgesState(initialEdges); + + const handleNodesChange = useCallback( + (changes: NodeChange[]) => { + // @ts-expect-error TODO: Figure out internal type errors in ReactFlow + onNodesChange(changes); + }, + [onNodesChange] + ); + + const handleEdgesChange = useCallback( + (changes: EdgeChange[]) => { + // @ts-expect-error TODO: Figure out internal type errors in ReactFlow + onEdgesChange(changes); + }, + [onEdgesChange] + ); + + const handleCreateOperation = useCallback(() => { + // TODO: calculate insert location + controller.promptCreateOperation(viewport.x, viewport.y); + }, [controller, viewport]); const proOptions: ProOptions = { hideAttribution: true }; - - const canvasWidth = useMemo(() => { - return 'calc(100vw - 1rem)'; - }, []); - + const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []); const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); + const OssNodeTypes: NodeTypes = useMemo( + () => ({ + synthesis: OperationNode, + input: InputNode + }), + [] + ); + return ( -
- -
+ + + + +
+ +
+
); } diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx new file mode 100644 index 00000000..cdf16dc8 --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx @@ -0,0 +1,37 @@ +import clsx from 'clsx'; + +import { IconNewItem } from '@/components/Icons'; +import BadgeHelp from '@/components/info/BadgeHelp'; +import MiniButton from '@/components/ui/MiniButton'; +import { HelpTopic } from '@/models/miscellaneous'; +import { PARAMETER } from '@/utils/constants'; + +import { useOssEdit } from '../OssEditContext'; + +interface ToolbarOssGraphProps { + onCreate: () => void; +} + +function ToolbarOssGraph({ onCreate }: ToolbarOssGraphProps) { + const controller = useOssEdit(); + + return ( +
+ {controller.isMutable ? ( + } + disabled={controller.isProcessing} + onClick={onCreate} + /> + ) : null} + +
+ ); +} + +export default ToolbarOssGraph; diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index 1b3317a3..6d45c53b 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -11,7 +11,8 @@ import { useOSS } from '@/context/OssContext'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgEditEditors from '@/dialogs/DlgEditEditors'; import { AccessPolicy } from '@/models/library'; -import { IOperationSchema } from '@/models/oss'; +import { Position2D } from '@/models/miscellaneous'; +import { IOperationCreateData, IOperationSchema } from '@/models/oss'; import { UserID, UserLevel } from '@/models/user'; import { information } from '@/utils/labels'; @@ -28,6 +29,8 @@ export interface IOssEditContext { toggleSubscribe: () => void; share: () => void; + + promptCreateOperation: (x: number, y: number) => void; } const OssEditContext = createContext(null); @@ -59,6 +62,9 @@ export const OssEditState = ({ children }: OssEditStateProps) => { const [showEditEditors, setShowEditEditors] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false); + const [showCreateOperation, setShowCreateOperation] = useState(false); + const [insertPosition, setInsertPosition] = useState({ x: 0, y: 0 }); + useLayoutEffect( () => setAccessLevel(prev => { @@ -136,6 +142,18 @@ export const OssEditState = ({ children }: OssEditStateProps) => { [model] ); + const promptCreateOperation = useCallback((x: number, y: number) => { + setInsertPosition({ x: x, y: y }); + setShowCreateOperation(true); + }, []); + + const handleCreateOperation = useCallback( + (data: IOperationCreateData) => { + model.createOperation(data, operation => toast.success(information.newOperation(operation.alias))); + }, + [model] + ); + return ( { promptEditors, promptLocation, - share + share, + + promptCreateOperation }} > {model.schema ? ( diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx index 52c94614..dfa6e313 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx @@ -25,6 +25,7 @@ import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library'; +import { ICstSubstituteData } from '@/models/oss'; import { ConstituentaID, CstType, @@ -33,7 +34,6 @@ import { ICstCreateData, ICstMovetoData, ICstRenameData, - ICstSubstituteData, ICstUpdateData, IInlineSynthesisData, IRSForm, diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index 0a189366..e01c0810 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -909,8 +909,9 @@ export const information = { addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, newLibraryItem: 'Схема успешно создана', - newConstituent: (alias: string) => `Конституента добавлена: ${alias}`, newVersion: (version: string) => `Версия создана: ${version}`, + newConstituent: (alias: string) => `Конституента добавлена: ${alias}`, + newOperation: (alias: string) => `Операция добавлена: ${alias}`, renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`, versionDestroyed: 'Версия удалена',