Implementing basic oss graph pt1

This commit is contained in:
Ivan 2024-07-20 18:26:32 +03:00
parent f91f42ff5b
commit 286abaf476
22 changed files with 395 additions and 151 deletions

View File

@ -36,6 +36,7 @@ This readme file is used mostly to document project dependencies
- react-error-boundary - react-error-boundary
- react-pdf - react-pdf
- react-tooltip - react-tooltip
- reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
- framer-motion - framer-motion
@ -54,6 +55,7 @@ This readme file is used mostly to document project dependencies
- autoprefixer - autoprefixer
- eslint-plugin-simple-import-sort - eslint-plugin-simple-import-sort
- eslint-plugin-tsdoc - eslint-plugin-tsdoc
- vite
- jest - jest
- ts-jest - ts-jest
- @types/jest - @types/jest

View File

@ -2,7 +2,7 @@
from apps.rsform.serializers import LibraryItemSerializer from apps.rsform.serializers import LibraryItemSerializer
from .basics import OperationPositionSerializer, PositionsSerializer from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,

View File

@ -14,3 +14,15 @@ class PositionsSerializer(serializers.Serializer):
positions = serializers.ListField( positions = serializers.ListField(
child=OperationPositionSerializer() 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()

View File

@ -10,7 +10,7 @@ from apps.rsform.serializers import LibraryItemDetailsSerializer
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType from ..models import Argument, Operation, OperationSchema, OperationType
from .basics import OperationPositionSerializer from .basics import OperationPositionSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer): class OperationSerializer(serializers.ModelSerializer):
@ -75,9 +75,12 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
items = serializers.ListField( items = serializers.ListField(
child=OperationSerializer() child=OperationSerializer()
) )
graph = serializers.ListField( arguments = serializers.ListField(
child=ArgumentSerializer() child=ArgumentSerializer()
) )
substitutions = serializers.ListField(
child=SubstitutionExSerializer()
)
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -90,15 +93,15 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
result['items'] = [] result['items'] = []
for operation in oss.operations(): for operation in oss.operations():
result['items'].append(OperationSerializer(operation).data) result['items'].append(OperationSerializer(operation).data)
result['graph'] = [] result['arguments'] = []
for argument in oss.arguments(): for argument in oss.arguments():
result['graph'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = [] result['substitutions'] = []
for substitution in oss.substitutions().values( for substitution in oss.substitutions().values(
'operation', 'operation',
'original', 'original',
'transfer_term',
'substitution', 'substitution',
'transfer_term',
original_alias=F('original__alias'), original_alias=F('original__alias'),
original_term=F('original__term_resolved'), original_term=F('original__term_resolved'),
substitution_alias=F('substitution__alias'), substitution_alias=F('substitution__alias'),

View File

@ -77,12 +77,12 @@ class TestOssViewset(EndpointTester):
self.assertEqual(sub['substitution_alias'], self.ks2x1.alias) self.assertEqual(sub['substitution_alias'], self.ks2x1.alias)
self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved) self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved)
graph = response.data['graph'] arguments = response.data['arguments']
self.assertEqual(len(graph), 2) self.assertEqual(len(arguments), 2)
self.assertEqual(graph[0]['operation'], self.operation3.pk) self.assertEqual(arguments[0]['operation'], self.operation3.pk)
self.assertEqual(graph[0]['argument'], self.operation1.pk) self.assertEqual(arguments[0]['argument'], self.operation1.pk)
self.assertEqual(graph[1]['operation'], self.operation3.pk) self.assertEqual(arguments[1]['operation'], self.operation3.pk)
self.assertEqual(graph[1]['argument'], self.operation2.pk) self.assertEqual(arguments[1]['argument'], self.operation2.pk)
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)

View File

@ -7,17 +7,14 @@ import { toast } from 'react-toastify';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library';
AccessPolicy,
ILibraryItem,
ILibraryUpdateData,
ITargetAccessPolicy,
ITargetLocation,
IVersionData,
LibraryItemType
} from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss'; import {
ICstSubstituteData,
IOperationCreateData,
IOperationCreatedResponse,
IOperationSchemaData
} from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
IConstituentaMeta, IConstituentaMeta,
@ -25,7 +22,6 @@ import {
ICstCreatedResponse, ICstCreatedResponse,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IProduceStructureResponse, IProduceStructureResponse,
@ -233,30 +229,6 @@ export function postCloneLibraryItem(target: string, request: FrontExchange<IRSF
}); });
} }
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
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<IRSFormData>) { export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) { if (!version) {
AxiosGet({ AxiosGet({
@ -357,7 +329,7 @@ export function getTRSFile(target: string, version: string, request: FrontPull<B
} }
} }
export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) { export function postCreateConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
AxiosPost({ AxiosPost({
endpoint: `/api/rsforms/${schema}/cst-create`, endpoint: `/api/rsforms/${schema}/cst-create`,
request: request request: request
@ -445,6 +417,23 @@ export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData
}); });
} }
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
AxiosGet({
endpoint: `/api/oss/${target}/details`,
request: request
});
}
export function postCreateOperation(
schema: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) {
AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`,
request: request
});
}
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) { export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({ AxiosPost({
endpoint: `/api/cctext/inflect`, endpoint: `/api/cctext/inflect`,

View File

@ -8,7 +8,7 @@ import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; 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 { describeConstituenta } from '@/utils/labels';
import { import {
@ -34,11 +34,11 @@ interface PickSubstitutionsProps {
filter1?: (cst: IConstituenta) => boolean; filter1?: (cst: IConstituenta) => boolean;
filter2?: (cst: IConstituenta) => boolean; filter2?: (cst: IConstituenta) => boolean;
items: ISubstitution[]; items: ISingleSubstitution[];
setItems: React.Dispatch<React.SetStateAction<ISubstitution[]>>; setItems: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
function SubstitutionIcon({ item }: { item: ISubstitution }) { function SubstitutionIcon({ item }: { item: ISingleSubstitution }) {
if (item.deleteRight) { if (item.deleteRight) {
if (item.takeLeftTerm) { if (item.takeLeftTerm) {
return <IconPageRight size='1.2rem' />; return <IconPageRight size='1.2rem' />;
@ -54,7 +54,7 @@ function SubstitutionIcon({ item }: { item: ISubstitution }) {
} }
} }
const columnHelper = createColumnHelper<ISubstitution>(); const columnHelper = createColumnHelper<ISingleSubstitution>();
function PickSubstitutions({ function PickSubstitutions({
items, items,
@ -80,7 +80,7 @@ function PickSubstitutions({
if (!leftCst || !rightCst) { if (!leftCst || !rightCst) {
return; return;
} }
const newSubstitution: ISubstitution = { const newSubstitution: ISingleSubstitution = {
leftCst: leftCst, leftCst: leftCst,
rightCst: rightCst, rightCst: rightCst,
deleteRight: deleteRight, deleteRight: deleteRight,
@ -99,7 +99,7 @@ function PickSubstitutions({
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(row: number) => { (row: number) => {
setItems(prev => { setItems(prev => {
const newItems: ISubstitution[] = []; const newItems: ISingleSubstitution[] = [];
prev.forEach((item, index) => { prev.forEach((item, index) => {
if (index !== row) { if (index !== row) {
newItems.push(item); newItems.push(item);

View File

@ -10,13 +10,14 @@ import {
patchSetAccessPolicy, patchSetAccessPolicy,
patchSetLocation, patchSetLocation,
patchSetOwner, patchSetOwner,
postCreateOperation,
postSubscribe postSubscribe
} from '@/app/backendAPI'; } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails'; import useOssDetails from '@/hooks/useOssDetails';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } 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 { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -43,6 +44,8 @@ interface IOssContext {
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void; setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void; setLocation: (newLocation: string, callback?: () => void) => void;
setEditors: (newEditors: UserID[], callback?: () => void) => void; setEditors: (newEditors: UserID[], callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
} }
const OssContext = createContext<IOssContext | null>(null); const OssContext = createContext<IOssContext | null>(null);
@ -63,13 +66,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const { const {
schema: schema, // prettier: split lines schema, // prettier: split lines
error: errorLoading, error: errorLoading,
setSchema, setSchema,
loading loading
} = useOssDetails({ } = useOssDetails({ target: itemID });
target: itemID
});
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
@ -249,6 +250,24 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, schema] [itemID, schema]
); );
const createOperation = useCallback(
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => {
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 ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
@ -267,7 +286,9 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setOwner, setOwner,
setEditors, setEditors,
setAccessPolicy, setAccessPolicy,
setLocation setLocation,
createOperation
}} }}
> >
{children} {children}

View File

@ -24,14 +24,15 @@ import {
patchSubstituteConstituents, patchSubstituteConstituents,
patchUploadTRS, patchUploadTRS,
patchVersion, patchVersion,
postCreateConstituenta,
postCreateVersion, postCreateVersion,
postNewConstituenta,
postSubscribe postSubscribe
} from '@/app/backendAPI'; } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library'; import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import { import {
ConstituentaID, ConstituentaID,
IConstituentaList, IConstituentaList,
@ -39,7 +40,6 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IRSForm, IRSForm,
@ -399,7 +399,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
const cstCreate = useCallback( const cstCreate = useCallback(
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => { (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined); setProcessingError(undefined);
postNewConstituenta(itemID, { postCreateConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,

View File

@ -8,7 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IInlineSynthesisData, IRSForm, ISubstitution } from '@/models/rsform'; import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform';
import TabConstituents from './TabConstituents'; import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema'; import TabSchema from './TabSchema';
@ -30,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined); const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<LibraryItemID[]>([]); const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]); const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });

View File

@ -2,7 +2,7 @@
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import DataLoader from '@/components/wrap/DataLoader'; 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 { prefixes } from '@/utils/constants';
import PickSubstitutions from '../../components/select/PickSubstitutions'; import PickSubstitutions from '../../components/select/PickSubstitutions';
@ -15,8 +15,8 @@ interface TabSubstitutionsProps {
loading?: boolean; loading?: boolean;
error?: ErrorData; error?: ErrorData;
substitutions: ISubstitution[]; substitutions: ISingleSubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
function TabSubstitutions({ function TabSubstitutions({

View File

@ -6,7 +6,8 @@ import { useMemo, useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions'; import PickSubstitutions from '@/components/select/PickSubstitutions';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { useRSForm } from '@/context/RSFormContext'; 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'; import { prefixes } from '@/utils/constants';
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> { interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
@ -16,7 +17,7 @@ interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) { function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]); const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]); const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);

View File

@ -2,22 +2,40 @@
* Module: OSS data loading and processing. * 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}. * Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
* *
*/ */
export class OssLoader { export class OssLoader {
private schema: IOperationSchemaData; private oss: IOperationSchemaData;
private graph: Graph = new Graph();
private operationByID: Map<OperationID, IOperation> = new Map();
constructor(input: IOperationSchemaData) { constructor(input: IOperationSchemaData) {
this.schema = input; this.oss = input;
} }
produceOSS(): IOperationSchema { produceOSS(): IOperationSchema {
const result = this.schema as IOperationSchema; const result = this.oss as IOperationSchema;
result.producedData = [1, 2, 3]; // TODO: put data processing here this.prepareLookups();
this.createGraph();
result.operationByID = this.operationByID;
result.graph = this.graph;
return result; 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));
}
} }

View File

@ -177,3 +177,11 @@ export interface GraphFilterParams {
allowConstant: boolean; allowConstant: boolean;
allowTheorem: boolean; allowTheorem: boolean;
} }
/**
* Represents XY Position.
*/
export interface Position2D {
x: number;
y: number;
}

View File

@ -2,18 +2,101 @@
* Module: Schema of Synthesis Operations. * 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<IOperation, 'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y'> {}
/**
* 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 { 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 { export interface IOperationSchema extends IOperationSchemaData {
producedData: number[]; // TODO: modify this to store calculated state on load graph: Graph;
operationByID: Map<OperationID, IOperation>;
}
/**
* Represents data response when creating {@link IOperation}.
*/
export interface IOperationCreatedResponse {
new_operation: IOperation;
oss: IOperationSchemaData;
} }

View File

@ -5,10 +5,11 @@
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library';
import { ICstSubstitute } from './oss';
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'; import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
/** /**
* Represents Constituenta type. * Represents {@link IConstituenta} type.
*/ */
export enum CstType { export enum CstType {
BASE = 'basic', BASE = 'basic',
@ -21,7 +22,7 @@ export enum CstType {
THEOREM = 'theorem' 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; export const CATEGORY_CST_TYPE = CstType.THEOREM;
/** /**
@ -30,7 +31,7 @@ export const CATEGORY_CST_TYPE = CstType.THEOREM;
export type Position = number; export type Position = number;
/** /**
* Represents {@link Constituenta} identifier type. * Represents {@link IConstituenta} identifier type.
*/ */
export type ConstituentaID = number; 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 export interface ICstCreateData
extends Pick< 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 { export interface ICstMovetoData extends IConstituentaList {
move_to: Position; move_to: Position;
@ -158,32 +159,6 @@ export interface ICstUpdateData
*/ */
export interface ICstRenameData extends ITargetCst, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {} export interface ICstRenameData extends ITargetCst, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {}
/**
* 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}. * Represents data response when creating {@link IConstituenta}.
*/ */
@ -265,6 +240,16 @@ export interface IVersionCreatedResponse {
schema: IRSFormData; 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. * Represents input data for inline synthesis.
*/ */

View File

@ -2,19 +2,12 @@
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useOssEdit } from '../OssEditContext';
import OssFlow from './OssFlow'; import OssFlow from './OssFlow';
function EditorOssGraph() { function EditorOssGraph() {
const controller = useOssEdit();
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<AnimateFade> <OssFlow />
<OssFlow controller={controller} />
</AnimateFade>
</ReactFlowProvider> </ReactFlowProvider>
); );
} }

View File

@ -1,49 +1,120 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { NodeTypes, ProOptions, ReactFlow } from 'reactflow'; 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 { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import { IOssEditContext } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import InputNode from './InputNode'; import InputNode from './InputNode';
import OperationNode from './OperationNode'; import OperationNode from './OperationNode';
import ToolbarOssGraph from './ToolbarOssGraph';
const OssNodeTypes: NodeTypes = { function OssFlow() {
synthesis: OperationNode,
input: InputNode
};
interface OssFlowProps {
controller: IOssEditContext;
}
function OssFlow({ controller }: OssFlowProps) {
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const model = useOSS(); const model = useOSS();
const controller = useOssEdit();
const viewport = useViewport();
console.log(model.loading);
console.log(controller.isMutable); console.log(controller.isMutable);
const initialNodes = [ const initialNodes: Node[] = useMemo(
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'input' }, () =>
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' }, type: 'synthesis' } !model.schema
]; ? []
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; : 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<Node>(initialNodes);
const [edges, onEdgesChange] = useEdgesState<Edge>(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 proOptions: ProOptions = { hideAttribution: true };
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
const canvasWidth = useMemo(() => {
return 'calc(100vw - 1rem)';
}, []);
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
const OssNodeTypes: NodeTypes = useMemo(
() => ({
synthesis: OperationNode,
input: InputNode
}),
[]
);
return ( return (
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}> <AnimateFade>
<ReactFlow nodes={initialNodes} edges={initialEdges} fitView proOptions={proOptions} nodeTypes={OssNodeTypes} /> <Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
</div> <ToolbarOssGraph onCreate={handleCreateOperation} />
</Overlay>
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
fitView
proOptions={proOptions}
nodeTypes={OssNodeTypes}
/>
</div>
</AnimateFade>
); );
} }

View File

@ -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 (
<div className='cc-icons'>
{controller.isMutable ? (
<MiniButton
title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={onCreate}
/>
) : null}
<BadgeHelp
topic={HelpTopic.UI_OSS_GRAPH}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
offset={4}
/>
</div>
);
}
export default ToolbarOssGraph;

View File

@ -11,7 +11,8 @@ import { useOSS } from '@/context/OssContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditEditors from '@/dialogs/DlgEditEditors';
import { AccessPolicy } from '@/models/library'; 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 { UserID, UserLevel } from '@/models/user';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
@ -28,6 +29,8 @@ export interface IOssEditContext {
toggleSubscribe: () => void; toggleSubscribe: () => void;
share: () => void; share: () => void;
promptCreateOperation: (x: number, y: number) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -59,6 +62,9 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
const [showEditEditors, setShowEditEditors] = useState(false); const [showEditEditors, setShowEditEditors] = useState(false);
const [showEditLocation, setShowEditLocation] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false);
const [showCreateOperation, setShowCreateOperation] = useState(false);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
useLayoutEffect( useLayoutEffect(
() => () =>
setAccessLevel(prev => { setAccessLevel(prev => {
@ -136,6 +142,18 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
[model] [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 ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
@ -149,7 +167,9 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
promptEditors, promptEditors,
promptLocation, promptLocation,
share share,
promptCreateOperation
}} }}
> >
{model.schema ? ( {model.schema ? (

View File

@ -25,6 +25,7 @@ import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst'; import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library'; import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import { import {
ConstituentaID, ConstituentaID,
CstType, CstType,
@ -33,7 +34,6 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IRSForm, IRSForm,

View File

@ -909,8 +909,9 @@ export const information = {
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
newLibraryItem: 'Схема успешно создана', newLibraryItem: 'Схема успешно создана',
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newVersion: (version: string) => `Версия создана: ${version}`, newVersion: (version: string) => `Версия создана: ${version}`,
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`, renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`,
versionDestroyed: 'Версия удалена', versionDestroyed: 'Версия удалена',