F: Implementing references

This commit is contained in:
Ivan 2025-08-04 22:58:08 +03:00
parent 88f8c4523a
commit 9c51e368a5
41 changed files with 783 additions and 147 deletions

View File

@ -128,6 +128,7 @@
"perfectivity", "perfectivity",
"PNCT", "PNCT",
"ponomarev", "ponomarev",
"popleft",
"PRCL", "PRCL",
"PRTF", "PRTF",
"PRTS", "PRTS",

View File

@ -23,6 +23,7 @@ from apps.rsform.models import (
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .Reference import Reference
from .Substitution import Substitution from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]] CstMapping = dict[str, Optional[Constituenta]]
@ -36,32 +37,31 @@ class OperationSchemaCached:
self.model = model self.model = model
self.cache = OssCache(self) self.cache = OssCache(self)
def delete_reference(self, target: Operation, keep_connections: bool = False): def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
''' Delete Reference Operation. ''' ''' Delete Reference Operation. '''
if keep_connections: if not keep_connections:
referred_operations = target.getQ_reference_target() self.delete_operation(target, keep_constituents)
if len(referred_operations) == 1: return
referred_operation = referred_operations[0] self.cache.ensure_loaded_subs()
for arg in target.getQ_as_argument(): operation = self.cache.operation_by_id[target]
arg.pk = None reference_target = self.cache.reference_target.get(target)
arg.argument = referred_operation if reference_target:
arg.save() for arg in operation.getQ_as_argument():
else: arg.argument_id = reference_target
pass arg.save()
# if target.result_id is not None: self.cache.remove_operation(target)
# self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema operation.delete()
target.delete()
def delete_operation(self, target: int, keep_constituents: bool = False): def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. ''' ''' Delete Operation. '''
self.cache.ensure_loaded_subs() self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target] operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation)
children = self.cache.graph.outputs[target] children = self.cache.graph.outputs[target]
if schema is not None and len(children) > 0: if operation.result is not None and len(children) > 0:
ids = [cst.pk for cst in schema.cache.constituents] ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
if not keep_constituents: if not keep_constituents:
self.before_delete_cst(schema.model.pk, ids) self._cascade_delete_inherited(operation.pk, ids)
else: else:
inheritance_to_delete: list[Inheritance] = [] inheritance_to_delete: list[Inheritance] = []
for child_id in children: for child_id in children:
@ -707,17 +707,23 @@ class OssCache:
self._schemas: list[RSFormCached] = [] self._schemas: list[RSFormCached] = []
self._schema_by_id: dict[int, RSFormCached] = {} self._schema_by_id: dict[int, RSFormCached] = {}
self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id')) self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id', 'operation_type'))
self.operation_by_id = {operation.pk: operation for operation in self.operations} self.operation_by_id = {operation.pk: operation for operation in self.operations}
self.graph = Graph[int]() self.graph = Graph[int]()
for operation in self.operations: for operation in self.operations:
self.graph.add_node(operation.pk) self.graph.add_node(operation.pk)
references = Reference.objects.filter(reference__oss=self._oss.model).only('reference_id', 'target_id')
self.reference_target = {ref.reference_id: ref.target_id for ref in references}
arguments = Argument.objects \ arguments = Argument.objects \
.filter(operation__oss=self._oss.model) \ .filter(operation__oss=self._oss.model) \
.only('operation_id', 'argument_id') \ .only('operation_id', 'argument_id') \
.order_by('order') .order_by('order')
for argument in arguments: for argument in arguments:
self.graph.add_edge(argument.argument_id, argument.operation_id) self.graph.add_edge(argument.argument_id, argument.operation_id)
target = self.reference_target.get(argument.argument_id)
if target is not None:
self.graph.add_edge(target, argument.operation_id)
self.is_loaded_subs = False self.is_loaded_subs = False
self.substitutions: dict[int, list[Substitution]] = {} self.substitutions: dict[int, list[Substitution]] = {}
@ -785,18 +791,12 @@ class OssCache:
schema.cache.ensure_loaded() schema.cache.ensure_loaded()
self._insert_new(schema) self._insert_new(schema)
def insert_operation(self, operation: Operation) -> None:
''' Insert new operation. '''
self.operations.append(operation)
self.operation_by_id[operation.pk] = operation
self.graph.add_node(operation.pk)
if self.is_loaded_subs:
self.substitutions[operation.pk] = []
self.inheritance[operation.pk] = []
def insert_argument(self, argument: Argument) -> None: def insert_argument(self, argument: Argument) -> None:
''' Insert new argument. ''' ''' Insert new argument. '''
self.graph.add_edge(argument.argument_id, argument.operation_id) self.graph.add_edge(argument.argument_id, argument.operation_id)
target = self.reference_target.get(argument.argument_id)
if target is not None:
self.graph.add_edge(target, argument.operation_id)
def insert_inheritance(self, inheritance: Inheritance) -> None: def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. ''' ''' Insert new inheritance. '''
@ -832,6 +832,8 @@ class OssCache:
del self._schema_by_id[target.result_id] del self._schema_by_id[target.result_id]
self.operations.remove(self.operation_by_id[operation]) self.operations.remove(self.operation_by_id[operation])
del self.operation_by_id[operation] del self.operation_by_id[operation]
if operation in self.reference_target:
del self.reference_target[operation]
if self.is_loaded_subs: if self.is_loaded_subs:
del self.substitutions[operation] del self.substitutions[operation]
del self.inheritance[operation] del self.inheritance[operation]
@ -839,6 +841,10 @@ class OssCache:
def remove_argument(self, argument: Argument) -> None: def remove_argument(self, argument: Argument) -> None:
''' Remove argument from cache. ''' ''' Remove argument from cache. '''
self.graph.remove_edge(argument.argument_id, argument.operation_id) self.graph.remove_edge(argument.argument_id, argument.operation_id)
target = self.reference_target.get(argument.argument_id)
if target is not None:
if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
self.graph.remove_edge(target, argument.operation_id)
def remove_substitution(self, target: Substitution) -> None: def remove_substitution(self, target: Substitution) -> None:
''' Remove substitution from cache. ''' ''' Remove substitution from cache. '''

View File

@ -16,6 +16,7 @@ from .data_access import (
MoveItemsSerializer, MoveItemsSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,
ReferenceSerializer,
RelocateConstituentsSerializer, RelocateConstituentsSerializer,
SetOperationInputSerializer, SetOperationInputSerializer,
TargetOperationSerializer, TargetOperationSerializer,

View File

@ -13,7 +13,16 @@ from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import Argument, Block, Inheritance, Layout, Operation, OperationType, Substitution from ..models import (
Argument,
Block,
Inheritance,
Layout,
Operation,
OperationType,
Reference,
Substitution
)
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
@ -45,6 +54,14 @@ class ArgumentSerializer(StrictModelSerializer):
fields = ('operation', 'argument') fields = ('operation', 'argument')
class ReferenceSerializer(StrictModelSerializer):
''' Serializer: Reference data. '''
class Meta:
''' serializer metadata. '''
model = Reference
fields = ('reference', 'target')
class CreateBlockSerializer(StrictSerializer): class CreateBlockSerializer(StrictSerializer):
''' Serializer: Block creation. ''' ''' Serializer: Block creation. '''
class BlockCreateData(StrictModelSerializer): class BlockCreateData(StrictModelSerializer):
@ -444,6 +461,7 @@ class DeleteReferenceSerializer(StrictSerializer):
) )
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type')) target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type'))
keep_connections = serializers.BooleanField(default=False, required=False) keep_connections = serializers.BooleanField(default=False, required=False)
keep_constituents = serializers.BooleanField(default=False, required=False)
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
@ -517,6 +535,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionExSerializer() child=SubstitutionExSerializer()
) )
references = serializers.ListField(
child=ReferenceSerializer()
)
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
) )
@ -534,6 +555,7 @@ class OperationSchemaSerializer(StrictModelSerializer):
result['blocks'] = [] result['blocks'] = []
result['arguments'] = [] result['arguments'] = []
result['substitutions'] = [] result['substitutions'] = []
result['references'] = []
for operation in Operation.objects.filter(oss=instance).order_by('pk'): for operation in Operation.objects.filter(oss=instance).order_by('pk'):
operation_data = OperationSerializer(operation).data operation_data = OperationSerializer(operation).data
operation_result = operation.result operation_result = operation.result
@ -556,6 +578,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitution_term=F('substitution__term_resolved'), substitution_term=F('substitution__term_resolved'),
).order_by('pk'): ).order_by('pk'):
result['substitutions'].append(substitution) result['substitutions'].append(substitution)
for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
result['references'].append(ReferenceSerializer(reference).data)
return result return result

View File

@ -1,5 +1,7 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Argument import * from .t_Argument import *
from .t_Inheritance import * from .t_Inheritance import *
from .t_Layout import *
from .t_Operation import * from .t_Operation import *
from .t_Reference import *
from .t_Substitution import * from .t_Substitution import *

View File

@ -0,0 +1,39 @@
''' Testing models: Layout. '''
from django.test import TestCase
from apps.library.models import LibraryItem
from apps.oss.models import Layout
class TestLayout(TestCase):
''' Testing Layout model. '''
def setUp(self):
self.library_item = LibraryItem.objects.create(alias='LIB1')
self.layout = Layout.objects.create(
oss=self.library_item,
data=[{'x': 1, 'y': 2}]
)
def test_str(self):
expected = f'Схема расположения {self.library_item.alias}'
self.assertEqual(str(self.layout), expected)
def test_update_data(self):
new_data = [{'x': 10, 'y': 20}]
Layout.update_data(self.library_item.id, new_data)
self.layout.refresh_from_db()
self.assertEqual(self.layout.data, new_data)
def test_default_data(self):
layout2 = Layout.objects.create(oss=self.library_item)
self.assertEqual(layout2.data, [])
def test_related_name_layout(self):
layouts = self.library_item.layout.all()
self.assertIn(self.layout, layouts)

View File

@ -0,0 +1,44 @@
''' Testing models: Reference. '''
from django.test import TestCase
from apps.oss.models import Operation, OperationSchema, OperationType, Reference
from apps.rsform.models import RSForm
class TestReference(TestCase):
''' Testing Reference model. '''
def setUp(self):
self.oss = OperationSchema.create(alias='T1')
self.operation1 = Operation.objects.create(
oss=self.oss.model,
alias='KS1',
operation_type=OperationType.INPUT,
)
self.operation2 = Operation.objects.create(
oss=self.oss.model,
operation_type=OperationType.REFERENCE,
)
self.reference = Reference.objects.create(
reference=self.operation2,
target=self.operation1
)
def test_str(self):
testStr = f'{self.operation2} -> {self.operation1}'
self.assertEqual(str(self.reference), testStr)
def test_cascade_delete_operation(self):
self.assertEqual(Reference.objects.count(), 1)
self.operation2.delete()
self.assertEqual(Reference.objects.count(), 0)
def test_cascade_delete_target(self):
self.assertEqual(Reference.objects.count(), 1)
self.operation1.delete()
self.assertEqual(Reference.objects.count(), 0)

View File

@ -2,4 +2,5 @@
from .t_attributes import * from .t_attributes import *
from .t_constituents import * from .t_constituents import *
from .t_operations import * from .t_operations import *
from .t_references import *
from .t_substitutions import * from .t_substitutions import *

View File

@ -0,0 +1,146 @@
''' Testing API: Propagate changes through references in OSS. '''
from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class ReferencePropagationTestCase(EndpointTester):
''' Test propagation through references in OSS. '''
def setUp(self):
super().setUp()
self.owned = OperationSchema.create(
title='Test',
alias='T1',
owner=self.user
)
self.owned_id = self.owned.model.pk
self.ks1 = RSForm.create(
alias='KS1',
title='Test1',
owner=self.user
)
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
self.ks2S1 = self.ks2.insert_last(
alias='S1',
definition_formal=r'(X1)',
convention='KS2S1'
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1.model
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.model
)
self.operation3 = self.owned.create_reference(self.operation1)
self.operation4 = self.owned.create_operation(
alias='4',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation4.pk, [{
'original': self.ks1X1,
'substitution': self.ks2S1
}])
self.owned.execute_operation(self.operation4)
self.operation4.refresh_from_db()
self.ks4 = RSForm(self.operation4.result)
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
self.ks4D2 = self.ks4.insert_last(
alias='D2',
definition_formal=r'X1 X2 X3 S1 D1',
convention='KS4D2'
)
self.operation5 = self.owned.create_operation(
alias='5',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5.pk, [{
'original': self.ks4X1,
'substitution': self.ks1X2
}])
self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db()
self.ks5 = RSForm(self.operation5.result)
self.ks5D4 = self.ks5.insert_last(
alias='D4',
definition_formal=r'X1 X2 X3 X4 S1 D1 D2 D3',
convention='KS5D4'
)
self.operation6 = self.owned.create_operation(
alias='6',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
self.owned.set_substitutions(self.operation6.pk, [{
'original': self.ks2X1,
'substitution': self.ks1X1
}])
self.owned.execute_operation(self.operation6)
self.operation6.refresh_from_db()
self.ks6 = RSForm(self.operation6.result)
self.ks6D2 = self.ks6.insert_last(
alias='D2',
definition_formal=r'X1 X2 X3 S1 D1',
convention='KS6D2'
)
self.layout_data = [
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
{'nodeID': 'o' + str(self.operation6.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
]
layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data
layout.save()
def test_reference_creation(self):
''' Test reference creation. '''
self.assertEqual(self.operation1.result, self.operation3.result)
self.assertEqual(self.ks1.constituentsQ().count(), 3)
self.assertEqual(self.ks2.constituentsQ().count(), 3)
self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituentsQ().count(), 9)
self.assertEqual(self.ks6.constituentsQ().count(), 6)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_target_propagation(self):
''' Test propagation when deleting a target operation. '''
data = {
'layout': self.layout_data,
'target': self.operation1.pk
}
self.executeOK(data=data, item=self.owned_id)
self.assertEqual(self.ks6.constituentsQ().count(), 4)
# self.assertEqual(self.ks5.constituentsQ().count(), 5)
# TODO: add more tests

View File

@ -683,7 +683,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item) oss = m.OperationSchemaCached(item)
m.Layout.update_data(pk, layout) m.Layout.update_data(pk, layout)
oss.delete_reference(operation, serializer.validated_data['keep_connections']) oss.delete_reference(operation.pk, serializer.validated_data['keep_connections'])
item.save(update_fields=['time_update']) item.save(update_fields=['time_update'])
return Response( return Response(

View File

@ -45,6 +45,11 @@ const DlgDeleteOperation = React.lazy(() =>
default: module.DlgDeleteOperation default: module.DlgDeleteOperation
})) }))
); );
const DlgDeleteReference = React.lazy(() =>
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
default: module.DlgDeleteReference
}))
);
const DlgEditEditors = React.lazy(() => const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({ import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
default: module.DlgEditEditors default: module.DlgEditEditors
@ -196,6 +201,8 @@ export const GlobalDialogs = () => {
return <DlgCreateVersion />; return <DlgCreateVersion />;
case DialogType.DELETE_OPERATION: case DialogType.DELETE_OPERATION:
return <DlgDeleteOperation />; return <DlgDeleteOperation />;
case DialogType.DELETE_REFERENCE:
return <DlgDeleteReference />;
case DialogType.GRAPH_PARAMETERS: case DialogType.GRAPH_PARAMETERS:
return <DlgGraphParams />; return <DlgGraphParams />;
case DialogType.RELOCATE_CONSTITUENTS: case DialogType.RELOCATE_CONSTITUENTS:

View File

@ -95,6 +95,8 @@ export { TbHexagonLetterD as IconCstTerm } from 'react-icons/tb';
export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb'; export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb';
export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb'; export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb';
export { TbHexagonLetterT as IconCstTheorem } from 'react-icons/tb'; export { TbHexagonLetterT as IconCstTheorem } from 'react-icons/tb';
export { PiArrowsMergeFill as IconSynthesis } from 'react-icons/pi';
export { VscReferences as IconReference } from 'react-icons/vsc';
export { LuNewspaper as IconDefinition } from 'react-icons/lu'; export { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu'; export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6'; export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
@ -129,7 +131,6 @@ export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi'; export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
export { LuPower as IconKeepAliasOn } from 'react-icons/lu'; export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu'; export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
export { VscReferences as IconPhantom } from 'react-icons/vsc';
// ===== Domain actions ===== // ===== Domain actions =====
export { BiUpvote as IconMoveUp } from 'react-icons/bi'; export { BiUpvote as IconMoveUp } from 'react-icons/bi';
@ -144,7 +145,6 @@ export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu'; export { LuReplace as IconReplace } from 'react-icons/lu';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa'; export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuCombine as IconSynthesis } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu'; export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr'; export { GrConnect as IconConnect } from 'react-icons/gr';

View File

@ -9,10 +9,12 @@ import {
type ICloneSchemaDTO, type ICloneSchemaDTO,
type IConstituentaReference, type IConstituentaReference,
type ICreateBlockDTO, type ICreateBlockDTO,
type ICreateReferenceDTO,
type ICreateSchemaDTO, type ICreateSchemaDTO,
type ICreateSynthesisDTO, type ICreateSynthesisDTO,
type IDeleteBlockDTO, type IDeleteBlockDTO,
type IDeleteOperationDTO, type IDeleteOperationDTO,
type IDeleteReferenceDTO,
type IImportSchemaDTO, type IImportSchemaDTO,
type IInputCreatedResponse, type IInputCreatedResponse,
type IMoveItemsDTO, type IMoveItemsDTO,
@ -87,6 +89,28 @@ export const ossApi = {
} }
}), }),
createReference: ({ itemID, data }: { itemID: number; data: ICreateReferenceDTO }) =>
axiosPost<ICreateReferenceDTO, IOperationCreatedResponse>({
schema: schemaOperationCreatedResponse,
endpoint: `/api/oss/${itemID}/create-reference`,
request: {
data: data,
successMessage: response => {
const alias = response.oss.operations.find(op => op.id === response.new_operation)?.alias;
return infoMsg.newOperation(alias ?? 'ОШИБКА');
}
}
}),
deleteReference: ({ itemID, data }: { itemID: number; data: IDeleteReferenceDTO }) =>
axiosPatch<IDeleteReferenceDTO, IOperationSchemaDTO>({
schema: schemaOperationSchema,
endpoint: `/api/oss/${itemID}/delete-reference`,
request: {
data: data,
successMessage: infoMsg.operationDestroyed
}
}),
createSchema: ({ itemID, data }: { itemID: number; data: ICreateSchemaDTO }) => createSchema: ({ itemID, data }: { itemID: number; data: ICreateSchemaDTO }) =>
axiosPost<ICreateSchemaDTO, IOperationCreatedResponse>({ axiosPost<ICreateSchemaDTO, IOperationCreatedResponse>({
schema: schemaOperationCreatedResponse, schema: schemaOperationCreatedResponse,

View File

@ -27,9 +27,10 @@ export class OssLoader {
private itemByNodeID = new Map<string, IOssItem>(); private itemByNodeID = new Map<string, IOssItem>();
private blockByID = new Map<number, IBlock>(); private blockByID = new Map<number, IBlock>();
private schemaIDs: number[] = []; private schemaIDs: number[] = [];
private extendedGraph = new Graph();
constructor(input: RO<IOperationSchemaDTO>) { constructor(input: RO<IOperationSchemaDTO>) {
this.oss = structuredClone(input) as IOperationSchema; this.oss = structuredClone(input) as unknown as IOperationSchema;
} }
produceOSS(): IOperationSchema { produceOSS(): IOperationSchema {
@ -47,6 +48,7 @@ export class OssLoader {
result.hierarchy = this.hierarchy; result.hierarchy = this.hierarchy;
result.schemas = this.schemaIDs; result.schemas = this.schemaIDs;
result.stats = this.calculateStats(); result.stats = this.calculateStats();
result.extendedGraph = this.extendedGraph;
return result; return result;
} }
@ -57,6 +59,7 @@ export class OssLoader {
this.itemByNodeID.set(operation.nodeID, operation); this.itemByNodeID.set(operation.nodeID, operation);
this.operationByID.set(operation.id, operation); this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id); this.graph.addNode(operation.id);
this.extendedGraph.addNode(operation.id);
this.hierarchy.addNode(operation.nodeID); this.hierarchy.addNode(operation.nodeID);
if (operation.parent) { if (operation.parent) {
this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID); this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID);
@ -75,7 +78,13 @@ export class OssLoader {
} }
private createGraph() { private createGraph() {
this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation)); this.oss.arguments.forEach(argument => {
this.graph.addEdge(argument.argument, argument.operation);
this.extendedGraph.addEdge(argument.argument, argument.operation);
});
this.oss.references.forEach(reference => {
this.extendedGraph.addEdge(reference.target, reference.reference);
});
} }
private extractSchemas() { private extractSchemas() {
@ -83,16 +92,37 @@ export class OssLoader {
} }
private inferOperationAttributes() { private inferOperationAttributes() {
const referenceCounts = new Map<number, number>();
this.graph.topologicalOrder().forEach(operationID => { this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!; const operation = this.operationByID.get(operationID)!;
const position = this.oss.layout.find(item => item.nodeID === operation.nodeID); const position = this.oss.layout.find(item => item.nodeID === operation.nodeID);
operation.x = position?.x ?? 0; operation.x = position?.x ?? 0;
operation.y = position?.y ?? 0; operation.y = position?.y ?? 0;
operation.is_consolidation = this.inferConsolidation(operationID); switch (operation.operation_type) {
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); case OperationType.INPUT:
operation.arguments = this.oss.arguments break;
.filter(item => item.operation === operationID) case OperationType.SYNTHESIS:
.map(item => item.argument); operation.is_consolidation = this.inferConsolidation(operationID);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
operation.arguments = this.oss.arguments
.filter(item => item.operation === operationID)
.map(item => item.argument);
break;
case OperationType.REFERENCE:
const ref = this.oss.references.find(item => item.reference === operationID);
const target = !!ref ? this.operationByID.get(ref.target) : null;
if (!target || !ref) {
throw new Error(`Reference ${operationID} not found`);
}
const refCount = (referenceCounts.get(target.id) ?? 0) + 1;
referenceCounts.set(target.id, refCount);
operation.target = ref.target;
operation.alias = `[${refCount}] ${target.alias}`;
operation.title = target.title;
operation.description = target.description;
break;
}
}); });
} }
@ -107,13 +137,13 @@ export class OssLoader {
} }
private inferConsolidation(operationID: number): boolean { private inferConsolidation(operationID: number): boolean {
const inputs = this.graph.expandInputs([operationID]); const inputs = this.extendedGraph.expandInputs([operationID]);
if (inputs.length === 0) { if (inputs.length === 0) {
return false; return false;
} }
const ancestors = [...inputs]; const ancestors = [...inputs];
inputs.forEach(input => { inputs.forEach(input => {
ancestors.push(...this.graph.expandAllInputs([input])); ancestors.push(...this.extendedGraph.expandAllInputs([input]));
}); });
const unique = new Set(ancestors); const unique = new Set(ancestors);
return unique.size < ancestors.length; return unique.size < ancestors.length;
@ -126,8 +156,11 @@ export class OssLoader {
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length, count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemaIDs.length, count_schemas: this.schemaIDs.length,
count_owned: operations.filter(item => !!item.result && !item.is_import).length, count_owned: operations.filter(
count_block: this.oss.blocks.length item => !!item.result && (item.operation_type !== OperationType.INPUT || !item.is_import)
).length,
count_block: this.oss.blocks.length,
count_references: this.oss.references.length
}; };
} }
} }

View File

@ -9,7 +9,8 @@ import { errorMsg } from '@/utils/labels';
/** Represents {@link IOperation} type. */ /** Represents {@link IOperation} type. */
export const OperationType = { export const OperationType = {
INPUT: 'input', INPUT: 'input',
SYNTHESIS: 'synthesis' SYNTHESIS: 'synthesis',
REFERENCE: 'reference'
} as const; } as const;
export type OperationType = (typeof OperationType)[keyof typeof OperationType]; export type OperationType = (typeof OperationType)[keyof typeof OperationType];
@ -48,6 +49,7 @@ export type IMoveItemsDTO = z.infer<typeof schemaMoveItems>;
/** Represents {@link IOperation} data, used in Create action. */ /** Represents {@link IOperation} data, used in Create action. */
export type ICreateSchemaDTO = z.infer<typeof schemaCreateSchema>; export type ICreateSchemaDTO = z.infer<typeof schemaCreateSchema>;
export type ICreateReferenceDTO = z.infer<typeof schemaCreateReference>;
export type ICreateSynthesisDTO = z.infer<typeof schemaCreateSynthesis>; export type ICreateSynthesisDTO = z.infer<typeof schemaCreateSynthesis>;
export type IImportSchemaDTO = z.infer<typeof schemaImportSchema>; export type IImportSchemaDTO = z.infer<typeof schemaImportSchema>;
export type ICloneSchemaDTO = z.infer<typeof schemaCloneSchema>; export type ICloneSchemaDTO = z.infer<typeof schemaCloneSchema>;
@ -61,6 +63,9 @@ export type IUpdateOperationDTO = z.infer<typeof schemaUpdateOperation>;
/** Represents {@link IOperation} data, used in Delete action. */ /** Represents {@link IOperation} data, used in Delete action. */
export type IDeleteOperationDTO = z.infer<typeof schemaDeleteOperation>; export type IDeleteOperationDTO = z.infer<typeof schemaDeleteOperation>;
/** Represents {@link IOperation} reference type data, used in Delete action. */
export type IDeleteReferenceDTO = z.infer<typeof schemaDeleteReference>;
/** Represents target {@link IOperation}. */ /** Represents target {@link IOperation}. */
export interface ITargetOperation { export interface ITargetOperation {
layout: IOssLayout; layout: IOssLayout;
@ -119,6 +124,11 @@ export const schemaBlock = z.strictObject({
parent: z.number().nullable() parent: z.number().nullable()
}); });
const schemaReference = z.strictObject({
target: z.number(),
reference: z.number()
});
export const schemaPosition = z.strictObject({ export const schemaPosition = z.strictObject({
x: z.number(), x: z.number(),
y: z.number(), y: z.number(),
@ -148,6 +158,7 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(), editors: z.number().array(),
operations: z.array(schemaOperation), operations: z.array(schemaOperation),
blocks: z.array(schemaBlock), blocks: z.array(schemaBlock),
references: z.array(schemaReference),
layout: schemaOssLayout, layout: schemaOssLayout,
arguments: z arguments: z
.object({ .object({
@ -188,6 +199,12 @@ export const schemaCreateSchema = z.strictObject({
position: schemaPosition position: schemaPosition
}); });
export const schemaCreateReference = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
position: schemaPosition
});
export const schemaCloneSchema = z.strictObject({ export const schemaCloneSchema = z.strictObject({
layout: schemaOssLayout, layout: schemaOssLayout,
source_operation: z.number(), source_operation: z.number(),
@ -230,6 +247,13 @@ export const schemaDeleteOperation = z.strictObject({
delete_schema: z.boolean() delete_schema: z.boolean()
}); });
export const schemaDeleteReference = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
keep_constituents: z.boolean(),
keep_connections: z.boolean()
});
export const schemaMoveItems = z.strictObject({ export const schemaMoveItems = z.strictObject({
layout: schemaOssLayout, layout: schemaOssLayout,
operations: z.array(z.number()), operations: z.array(z.number()),

View File

@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type ICreateReferenceDTO } from './types';
export const useCreateReference = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-reference'],
mutationFn: ossApi.createReference,
onSuccess: data => {
updateTimestamp(data.oss.id, data.oss.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
},
onError: () => client.invalidateQueries()
});
return {
createReference: (data: { itemID: number; data: ICreateReferenceDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -0,0 +1,29 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
import { KEYS } from '@/backend/configuration';
import { ossApi } from './api';
import { type IDeleteReferenceDTO } from './types';
export const useDeleteReference = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-reference'],
mutationFn: ossApi.deleteReference,
onSuccess: async data => {
updateTimestamp(data.id, data.time_update);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await Promise.allSettled([
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
client.invalidateQueries({ queryKey: [KEYS.rsform] })
]);
},
onError: () => client.invalidateQueries()
});
return {
deleteReference: (data: { itemID: number; data: IDeleteReferenceDTO }) => mutation.mutateAsync(data)
};
};

View File

@ -47,12 +47,12 @@ export function InfoOperation({ operation }: InfoOperationProps) {
<p> <p>
<b>Тип:</b> {labelOperationType(operation.operation_type)} <b>Тип:</b> {labelOperationType(operation.operation_type)}
</p> </p>
{operation.is_import ? ( {operation.operation_type === OperationType.INPUT && operation.is_import ? (
<p> <p>
<b>КС не принадлежит ОСС</b> <b>КС не принадлежит ОСС</b>
</p> </p>
) : null} ) : null}
{operation.is_consolidation ? ( {operation.operation_type === OperationType.SYNTHESIS && operation.is_consolidation ? (
<p> <p>
<b>Ромбовидный синтез</b> <b>Ромбовидный синтез</b>
</p> </p>
@ -69,7 +69,7 @@ export function InfoOperation({ operation }: InfoOperationProps) {
{operation.description} {operation.description}
</p> </p>
) : null} ) : null}
{operation.substitutions.length > 0 ? ( {operation.operation_type === OperationType.SYNTHESIS && operation.substitutions.length > 0 ? (
<DataTable <DataTable
dense dense
noHeader noHeader

View File

@ -1,6 +1,7 @@
import { import {
IconConceptBlock, IconConceptBlock,
IconDownload, IconDownload,
IconReference,
IconRSForm, IconRSForm,
IconRSFormImported, IconRSFormImported,
IconRSFormOwned, IconRSFormOwned,
@ -18,14 +19,16 @@ interface OssStatsProps {
export function OssStats({ className, stats }: OssStatsProps) { export function OssStats({ className, stats }: OssStatsProps) {
return ( return (
<aside className={cn('grid grid-cols-4 gap-1 justify-items-end h-min select-none', className)}> <aside className={cn('grid grid-cols-3 gap-1 justify-items-end h-min select-none', className)}>
<div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '> <div id='count_operations' className='w-fit flex gap-3 hover:cursor-default '>
<span>Всего</span> <span>Всего</span>
<span>{stats.count_all}</span> <span>{stats.count_all}</span>
</div> </div>
<ValueStats id='count_block' title='Блоки' icon={<IconConceptBlock size='1.25rem' />} value={stats.count_block} /> <ValueStats id='count_block' title='Блоки' icon={<IconConceptBlock size='1.25rem' />} value={stats.count_block} />
<ValueStats <ValueStats
id='count_inputs' id='count_inputs'
className='col-start-1'
title='Загрузка' title='Загрузка'
icon={<IconDownload size='1.25rem' />} icon={<IconDownload size='1.25rem' />}
value={stats.count_inputs} value={stats.count_inputs}
@ -36,6 +39,12 @@ export function OssStats({ className, stats }: OssStatsProps) {
icon={<IconSynthesis size='1.25rem' />} icon={<IconSynthesis size='1.25rem' />}
value={stats.count_synthesis} value={stats.count_synthesis}
/> />
<ValueStats
id='count_references'
title='Синтез'
icon={<IconReference size='1.25rem' />}
value={stats.count_references}
/>
<ValueStats <ValueStats
id='count_schemas' id='count_schemas'

View File

@ -18,6 +18,9 @@ export function TabArguments() {
} = useFormContext<ICreateSynthesisDTO>(); } = useFormContext<ICreateSynthesisDTO>();
const inputs = useWatch({ control, name: 'arguments' }); const inputs = useWatch({ control, name: 'arguments' });
const references = manager.oss.references.filter(item => inputs.includes(item.target)).map(item => item.reference);
const filtered = manager.oss.operations.filter(item => !references.includes(item.id));
return ( return (
<div className='cc-fade-in cc-column'> <div className='cc-fade-in cc-column'>
<TextInput <TextInput
@ -64,7 +67,7 @@ export function TabArguments() {
name='arguments' name='arguments'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<PickMultiOperation items={manager.oss.operations} value={field.value} onChange={field.onChange} rows={6} /> <PickMultiOperation items={filtered} value={field.value} onChange={field.onChange} rows={6} />
)} )}
/> />
</div> </div>

View File

@ -9,13 +9,13 @@ import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type IDeleteOperationDTO, type IOssLayout, schemaDeleteOperation } from '../backend/types'; import { type IDeleteOperationDTO, type IOssLayout, OperationType, schemaDeleteOperation } from '../backend/types';
import { useDeleteOperation } from '../backend/use-delete-operation'; import { useDeleteOperation } from '../backend/use-delete-operation';
import { type IOperation, type IOperationSchema } from '../models/oss'; import { type IOperationInput, type IOperationSchema, type IOperationSynthesis } from '../models/oss';
export interface DlgDeleteOperationProps { export interface DlgDeleteOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperationInput | IOperationSynthesis;
layout: IOssLayout; layout: IOssLayout;
} }
@ -54,13 +54,13 @@ export function DlgDeleteOperation() {
<Checkbox <Checkbox
label='Удалить схему' label='Удалить схему'
titleHtml={ titleHtml={
target.is_import || target.result === null (target.operation_type === OperationType.INPUT && target.is_import) || target.result === null
? 'Привязанную схему нельзя удалить' ? 'Привязанную схему нельзя удалить'
: 'Удалить схему вместе с операцией' : 'Удалить схему вместе с операцией'
} }
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
disabled={target.is_import || target.result === null} disabled={(target.operation_type === OperationType.INPUT && target.is_import) || target.result === null}
/> />
)} )}
/> />

View File

@ -0,0 +1,79 @@
'use client';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { HelpTopic } from '@/features/help';
import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { type IDeleteReferenceDTO, type IOssLayout, schemaDeleteReference } from '../backend/types';
import { useDeleteReference } from '../backend/use-delete-reference';
import { type IOperationReference, type IOperationSchema } from '../models/oss';
export interface DlgDeleteReferenceProps {
oss: IOperationSchema;
target: IOperationReference;
layout: IOssLayout;
}
export function DlgDeleteReference() {
const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteReferenceProps);
const { deleteReference } = useDeleteReference();
const { handleSubmit, control } = useForm<IDeleteReferenceDTO>({
resolver: zodResolver(schemaDeleteReference),
defaultValues: {
target: target.id,
layout: layout,
keep_constituents: false,
keep_connections: false
}
});
const keep_connections = useWatch({ control, name: 'keep_connections' });
function onSubmit(data: IDeleteReferenceDTO) {
return deleteReference({ itemID: oss.id, data: data });
}
return (
<ModalForm
overflowVisible
header='Удаление операции'
submitText='Подтвердить удаление'
onSubmit={event => void handleSubmit(onSubmit)(event)}
className='w-140 pb-3 px-6 cc-column select-none'
helpTopic={HelpTopic.CC_PROPAGATION}
>
<TextInput disabled dense noBorder id='operation_alias' label='Операция' value={target.alias} />
<Controller
control={control}
name='keep_connections'
render={({ field }) => (
<Checkbox
label='Переадресовать связи на оригинал'
titleHtml='Связи аргументов будут перенаправлены на оригинал ссылки'
value={field.value}
onChange={field.onChange}
disabled={target.result === null}
/>
)}
/>
<Controller
control={control}
name='keep_constituents'
render={({ field }) => (
<Checkbox
label='Сохранить наследованные конституенты'
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
value={field.value}
onChange={field.onChange}
disabled={target.result === null || keep_connections}
/>
)}
/>
</ModalForm>
);
}

View File

@ -13,7 +13,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types'; import { type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types';
import { useUpdateOperation } from '../../backend/use-update-operation'; import { useUpdateOperation } from '../../backend/use-update-operation';
import { type IOperation } from '../../models/oss'; import { type IOperationInput, type IOperationSynthesis } from '../../models/oss';
import { type LayoutManager } from '../../models/oss-layout-api'; import { type LayoutManager } from '../../models/oss-layout-api';
import { TabArguments } from './tab-arguments'; import { TabArguments } from './tab-arguments';
@ -22,7 +22,7 @@ import { TabSubstitutions } from './tab-substitutions';
export interface DlgEditOperationProps { export interface DlgEditOperationProps {
manager: LayoutManager; manager: LayoutManager;
target: IOperation; target: IOperationInput | IOperationSynthesis;
} }
export const TabID = { export const TabID = {
@ -46,11 +46,14 @@ export function DlgEditOperation() {
description: target.description, description: target.description,
parent: target.parent parent: target.parent
}, },
arguments: target.arguments, arguments: target.operation_type === OperationType.SYNTHESIS ? target.arguments : [],
substitutions: target.substitutions.map(sub => ({ substitutions:
original: sub.original, target.operation_type === OperationType.SYNTHESIS
substitution: sub.substitution ? target.substitutions.map(sub => ({
})), original: sub.original,
substitution: sub.substitution
}))
: [],
layout: manager.layout layout: manager.layout
}, },
mode: 'onChange' mode: 'onChange'

View File

@ -1,5 +1,5 @@
'use client'; 'use client';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { Label } from '@/components/input'; import { Label } from '@/components/input';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -12,7 +12,12 @@ import { type DlgEditOperationProps } from './dlg-edit-operation';
export function TabArguments() { export function TabArguments() {
const { control, setValue } = useFormContext<IUpdateOperationDTO>(); const { control, setValue } = useFormContext<IUpdateOperationDTO>();
const { manager, target } = useDialogsStore(state => state.props as DlgEditOperationProps); const { manager, target } = useDialogsStore(state => state.props as DlgEditOperationProps);
const potentialCycle = [target.id, ...manager.oss.graph.expandAllOutputs([target.id])]; const args = useWatch({ control, name: 'arguments' });
const references = manager.oss.references
.filter(item => args.includes(item.target) || item.target === target.id)
.map(item => item.reference);
const potentialCycle = [target.id, ...references, ...manager.oss.graph.expandAllOutputs([target.id])];
const filtered = manager.oss.operations.filter(item => !potentialCycle.includes(item.id)); const filtered = manager.oss.operations.filter(item => !potentialCycle.includes(item.id));
function handleChangeArguments(prev: number[], newValue: number[]) { function handleChangeArguments(prev: number[], newValue: number[]) {

View File

@ -11,12 +11,14 @@ import {
const labelOperationTypeRecord: Record<OperationType, string> = { const labelOperationTypeRecord: Record<OperationType, string> = {
[OperationType.INPUT]: 'Загрузка', [OperationType.INPUT]: 'Загрузка',
[OperationType.SYNTHESIS]: 'Синтез' [OperationType.SYNTHESIS]: 'Синтез',
[OperationType.REFERENCE]: 'Ссылка'
}; };
const describeOperationTypeRecord: Record<OperationType, string> = { const describeOperationTypeRecord: Record<OperationType, string> = {
[OperationType.INPUT]: 'Загрузка концептуальной схемы в ОСС', [OperationType.INPUT]: 'Загрузка концептуальной схемы в ОСС',
[OperationType.SYNTHESIS]: 'Синтез концептуальных схем' [OperationType.SYNTHESIS]: 'Синтез концептуальных схем',
[OperationType.REFERENCE]: 'Создание ссылки на результат операции'
}; };
/** Retrieves label for {@link OperationType}. */ /** Retrieves label for {@link OperationType}. */

View File

@ -4,7 +4,8 @@ import {
type ICreateSynthesisDTO, type ICreateSynthesisDTO,
type IImportSchemaDTO, type IImportSchemaDTO,
type INodePosition, type INodePosition,
type IOssLayout type IOssLayout,
OperationType
} from '../backend/types'; } from '../backend/types';
import { type IOperationSchema, NodeType } from './oss'; import { type IOperationSchema, NodeType } from './oss';
@ -258,7 +259,11 @@ export class LayoutManager {
} }
const freeInputs = this.oss.operations const freeInputs = this.oss.operations
.filter(operation => operation.arguments.length === 0 && operation.parent === null) .filter(
operation =>
operation.parent === null &&
(operation.operation_type !== OperationType.SYNTHESIS || operation.arguments.length === 0)
)
.map(operation => operation.nodeID); .map(operation => operation.nodeID);
let inputsPositions = this.layout.filter(pos => freeInputs.includes(pos.nodeID)); let inputsPositions = this.layout.filter(pos => freeInputs.includes(pos.nodeID));
if (inputsPositions.length === 0) { if (inputsPositions.length === 0) {

View File

@ -8,7 +8,8 @@ import {
type IBlockDTO, type IBlockDTO,
type ICstSubstituteInfo, type ICstSubstituteInfo,
type IOperationDTO, type IOperationDTO,
type IOperationSchemaDTO type IOperationSchemaDTO,
type OperationType
} from '../backend/types'; } from '../backend/types';
/** Represents OSS node type. */ /** Represents OSS node type. */
@ -18,26 +19,50 @@ export const NodeType = {
} as const; } as const;
export type NodeType = (typeof NodeType)[keyof typeof NodeType]; export type NodeType = (typeof NodeType)[keyof typeof NodeType];
/** Represents Operation. */ /** Represents OSS graph node. */
export interface IOperation extends IOperationDTO { export interface IOssNode {
nodeID: string; nodeID: string;
nodeType: typeof NodeType.OPERATION; nodeType: NodeType;
parent: number | null;
x: number; x: number;
y: number; y: number;
width: number;
height: number;
}
/** Represents Operation common attributes. */
export interface IOperationBase
extends IOssNode,
Pick<IOperationDTO, 'alias' | 'title' | 'description' | 'id' | 'operation_type' | 'result'> {
nodeType: typeof NodeType.OPERATION;
}
/** Represents Input Operation. */
export interface IOperationInput extends IOperationBase {
operation_type: typeof OperationType.INPUT;
is_import: boolean; is_import: boolean;
}
/** Represents Reference Operation. */
export interface IOperationReference extends IOperationBase {
operation_type: typeof OperationType.REFERENCE;
target: number;
}
/** Represents Synthesis Operation. */
export interface IOperationSynthesis extends IOperationBase {
operation_type: typeof OperationType.SYNTHESIS;
is_consolidation: boolean; // aka 'diamond synthesis' is_consolidation: boolean; // aka 'diamond synthesis'
substitutions: ICstSubstituteInfo[]; substitutions: ICstSubstituteInfo[];
arguments: number[]; arguments: number[];
} }
/** Represents Operation. */
export type IOperation = IOperationInput | IOperationReference | IOperationSynthesis;
/** Represents Block. */ /** Represents Block. */
export interface IBlock extends IBlockDTO { export interface IBlock extends IOssNode, IBlockDTO {
nodeID: string;
nodeType: typeof NodeType.BLOCK; nodeType: typeof NodeType.BLOCK;
x: number;
y: number;
width: number;
height: number;
} }
/** Represents item of OperationSchema. */ /** Represents item of OperationSchema. */
@ -51,14 +76,16 @@ export interface IOperationSchemaStats {
count_schemas: number; count_schemas: number;
count_owned: number; count_owned: number;
count_block: number; count_block: number;
count_references: number;
} }
/** Represents OperationSchema. */ /** Represents OperationSchema. */
export interface IOperationSchema extends IOperationSchemaDTO { export interface IOperationSchema extends Omit<IOperationSchemaDTO, 'operations'> {
operations: IOperation[]; operations: IOperation[];
blocks: IBlock[]; blocks: IBlock[];
graph: Graph; graph: Graph;
extendedGraph: Graph;
hierarchy: Graph<string>; hierarchy: Graph<string>;
schemas: number[]; schemas: number[];
stats: IOperationSchemaStats; stats: IOperationSchemaStats;

View File

@ -47,10 +47,10 @@ export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextM
margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'} margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'}
> >
{!!item ? ( {!!item ? (
item.nodeType === NodeType.OPERATION ? ( item.nodeType === NodeType.BLOCK ? (
<MenuOperation operation={item} onHide={onHide} />
) : (
<MenuBlock block={item} onHide={onHide} /> <MenuBlock block={item} onHide={onHide} />
) : (
<MenuOperation operation={item} onHide={onHide} />
) )
) : null} ) : null}
</Dropdown> </Dropdown>

View File

@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
import { useLibrary } from '@/features/library/backend/use-library'; import { useLibrary } from '@/features/library/backend/use-library';
import { useCloneSchema } from '@/features/oss/backend/use-clone-schema'; import { useCloneSchema } from '@/features/oss/backend/use-clone-schema';
import { useCreateReference } from '@/features/oss/backend/use-create-reference';
import { DropdownButton } from '@/components/dropdown'; import { DropdownButton } from '@/components/dropdown';
import { import {
@ -13,13 +14,13 @@ import {
IconEdit2, IconEdit2,
IconExecute, IconExecute,
IconNewRSForm, IconNewRSForm,
IconPhantom, IconReference,
IconRSForm IconRSForm
} from '@/components/icons'; } from '@/components/icons';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
import { notImplemented, prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../../backend/types'; import { OperationType } from '../../../../backend/types';
import { useCreateInput } from '../../../../backend/use-create-input'; import { useCreateInput } from '../../../../backend/use-create-input';
@ -44,11 +45,13 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
const { createInput: inputCreate } = useCreateInput(); const { createInput: inputCreate } = useCreateInput();
const { executeOperation: operationExecute } = useExecuteOperation(); const { executeOperation: operationExecute } = useExecuteOperation();
const { cloneSchema } = useCloneSchema(); const { cloneSchema } = useCloneSchema();
const { createReference } = useCreateReference();
const showEditInput = useDialogsStore(state => state.showChangeInputSchema); const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents); const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
const showEditOperation = useDialogsStore(state => state.showEditOperation); const showEditOperation = useDialogsStore(state => state.showEditOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showDeleteReference = useDialogsStore(state => state.showDeleteReference);
const readyForSynthesis = (() => { const readyForSynthesis = (() => {
if (operation?.operation_type !== OperationType.SYNTHESIS) { if (operation?.operation_type !== OperationType.SYNTHESIS) {
@ -92,7 +95,7 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
} }
function handleEditOperation() { function handleEditOperation() {
if (!operation) { if (!operation || operation.operation_type === OperationType.REFERENCE) {
return; return;
} }
onHide(); onHide();
@ -107,11 +110,22 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
return; return;
} }
onHide(); onHide();
showDeleteOperation({ switch (operation.operation_type) {
oss: schema, case OperationType.REFERENCE:
target: operation, showDeleteReference({
layout: getLayout() oss: schema,
}); target: operation,
layout: getLayout()
});
break;
case OperationType.INPUT:
case OperationType.SYNTHESIS:
showDeleteOperation({
oss: schema,
target: operation,
layout: getLayout()
});
}
} }
function handleOperationExecute() { function handleOperationExecute() {
@ -152,9 +166,24 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
}); });
} }
function handleCreatePhantom() { function handleCreateReference() {
onHide(); onHide();
notImplemented();
const layout = getLayout();
const manager = new LayoutManager(schema, layout);
const newPosition = manager.newClonePosition(operation.nodeID);
if (!newPosition) {
return;
}
void createReference({
itemID: schema.id,
data: {
target: operation.id,
layout: layout,
position: newPosition
}
}).then(response => setTimeout(() => setSelected([`o${response.new_operation}`]), PARAMETER.refreshTimeout));
} }
function handleClone() { function handleClone() {
@ -177,17 +206,34 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
}).then(response => setTimeout(() => setSelected([`o${response.new_operation}`]), PARAMETER.refreshTimeout)); }).then(response => setTimeout(() => setSelected([`o${response.new_operation}`]), PARAMETER.refreshTimeout));
} }
function handleSelectTarget() {
onHide();
if (operation.operation_type !== OperationType.REFERENCE) {
return;
}
setSelected([`o${operation.target}`]);
}
return ( return (
<> <>
<DropdownButton {operation.operation_type !== OperationType.REFERENCE ? (
text='Редактировать' <DropdownButton
title='Редактировать операцию' text='Редактировать'
icon={<IconEdit2 size='1rem' className='icon-primary' />} title='Редактировать операцию'
onClick={handleEditOperation} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={!isMutable || isProcessing} onClick={handleEditOperation}
/> disabled={!isMutable || isProcessing}
/>
) : (
<DropdownButton
text='Оригинал'
title='Выделить оригинал'
icon={<IconReference size='1rem' className='icon-primary' />}
onClick={handleSelectTarget}
/>
)}
{operation?.result ? ( {operation.result ? (
<DropdownButton <DropdownButton
text='Открыть схему' text='Открыть схему'
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')} titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
@ -197,7 +243,10 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
disabled={isProcessing} disabled={isProcessing}
/> />
) : null} ) : null}
{isMutable && !operation?.result && operation?.arguments.length === 0 ? ( {isMutable &&
!operation.result &&
operation.operation_type === OperationType.SYNTHESIS &&
operation.arguments.length === 0 ? (
<DropdownButton <DropdownButton
text='Создать схему' text='Создать схему'
title='Создать пустую схему' title='Создать пустую схему'
@ -215,7 +264,7 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
disabled={isProcessing} disabled={isProcessing}
/> />
) : null} ) : null}
{isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? ( {isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton <DropdownButton
text='Активировать синтез' text='Активировать синтез'
titleHtml={ titleHtml={
@ -230,7 +279,7 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
/> />
) : null} ) : null}
{isMutable && operation?.result ? ( {isMutable && operation.result && operation.operation_type !== OperationType.REFERENCE ? (
<DropdownButton <DropdownButton
text='Конституенты' text='Конституенты'
titleHtml='Перенос конституент</br>между схемами' titleHtml='Перенос конституент</br>между схемами'
@ -241,17 +290,17 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
/> />
) : null} ) : null}
{isMutable ? ( {isMutable && operation.operation_type !== OperationType.REFERENCE ? (
<DropdownButton <DropdownButton
text='Создать ссылку' text='Создать ссылку'
title='Создать ссылку на результат операции' title='Создать ссылку на результат операции'
icon={<IconPhantom size='1rem' className='icon-green' />} icon={<IconReference size='1rem' className='icon-green' />}
onClick={handleCreatePhantom} onClick={handleCreateReference}
disabled={isProcessing} disabled={isProcessing}
/> />
) : null} ) : null}
{isMutable ? ( {isMutable && operation.operation_type !== OperationType.REFERENCE ? (
<DropdownButton <DropdownButton
text='Клонировать' text='Клонировать'
title='Создать и загрузить копию концептуальной схемы' title='Создать и загрузить копию концептуальной схемы'

View File

@ -22,6 +22,8 @@ interface NodeCoreProps {
export function NodeCore({ node }: NodeCoreProps) { export function NodeCore({ node }: NodeCoreProps) {
const { selectedItems, schema } = useOssEdit(); const { selectedItems, schema } = useOssEdit();
const opType = node.data.operation.operation_type;
const focus = selectedItems.length === 1 ? selectedItems[0] : null; const focus = selectedItems.length === 1 ? selectedItems[0] : null;
const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.operation.nodeID)) ?? false; const isChild = (!!focus && schema.hierarchy.at(focus.nodeID)?.outputs.includes(node.data.operation.nodeID)) ?? false;
@ -36,6 +38,7 @@ export function NodeCore({ node }: NodeCoreProps) {
className={cn( className={cn(
'cc-node-operation h-[40px] w-[150px]', 'cc-node-operation h-[40px] w-[150px]',
'relative flex items-center justify-center p-[2px]', 'relative flex items-center justify-center p-[2px]',
opType === OperationType.REFERENCE && 'border-dashed',
isChild && 'border-accent-orange' isChild && 'border-accent-orange'
)} )}
> >
@ -45,7 +48,7 @@ export function NodeCore({ node }: NodeCoreProps) {
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'} title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
icon={<IconRSForm className={hasFile ? 'text-constructive' : 'text-destructive'} size='12px' />} icon={<IconRSForm className={hasFile ? 'text-constructive' : 'text-destructive'} size='12px' />}
/> />
{node.data.operation.is_consolidation ? ( {opType === OperationType.SYNTHESIS && node.data.operation.is_consolidation ? (
<Indicator <Indicator
noPadding noPadding
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент' titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
@ -66,11 +69,11 @@ export function NodeCore({ node }: NodeCoreProps) {
</div> </div>
) : null} ) : null}
{node.data.operation.operation_type === OperationType.INPUT ? ( {opType === OperationType.INPUT ? (
<div className='absolute top-[3px] right-1/2 translate-x-1/2 border-t w-[30px]' /> <div className='absolute top-[3px] right-1/2 translate-x-1/2 border-t w-[30px]' />
) : null} ) : null}
{node.data.operation.is_import ? ( {opType === OperationType.INPUT && node.data.operation.is_import ? (
<div className='absolute left-[3px] top-1/2 -translate-y-1/2 border-r rounded-none bg-input h-[22px]' /> <div className='absolute left-[3px] top-1/2 -translate-y-1/2 border-r rounded-none bg-input h-[22px]' />
) : null} ) : null}

View File

@ -2,10 +2,12 @@ import { type NodeTypes } from 'reactflow';
import { BlockNode } from './block-node'; import { BlockNode } from './block-node';
import { InputNode } from './input-node'; import { InputNode } from './input-node';
import { OperationNode } from './operation-node'; import { ReferenceNode } from './reference-node';
import { SynthesisNode } from './synthesis-node';
export const OssNodeTypes: NodeTypes = { export const OssNodeTypes: NodeTypes = {
synthesis: OperationNode,
input: InputNode, input: InputNode,
synthesis: SynthesisNode,
reference: ReferenceNode,
block: BlockNode block: BlockNode
} as const; } as const;

View File

@ -0,0 +1,14 @@
import { Handle, Position } from 'reactflow';
import { type OperationInternalNode } from '../../../../models/oss-layout';
import { NodeCore } from './node-core';
export function ReferenceNode(node: OperationInternalNode) {
return (
<>
<NodeCore node={node} />
<Handle type='source' position={Position.Bottom} className='-translate-y-[1px]' />
</>
);
}

View File

@ -6,7 +6,7 @@ import { type OperationInternalNode } from '../../../../models/oss-layout';
import { NodeCore } from './node-core'; import { NodeCore } from './node-core';
export function OperationNode(node: OperationInternalNode) { export function SynthesisNode(node: OperationInternalNode) {
return ( return (
<> <>
<Handle type='target' position={Position.Top} id='left' style={{ left: 40, top: -2 }} /> <Handle type='target' position={Position.Top} id='left' style={{ left: 40, top: -2 }} />

View File

@ -11,6 +11,7 @@ import { PARAMETER } from '@/utils/constants';
import { promptText } from '@/utils/labels'; import { promptText } from '@/utils/labels';
import { withPreventDefault } from '@/utils/utils'; import { withPreventDefault } from '@/utils/utils';
import { OperationType } from '../../../backend/types';
import { useDeleteBlock } from '../../../backend/use-delete-block'; import { useDeleteBlock } from '../../../backend/use-delete-block';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdateLayout } from '../../../backend/use-update-layout'; import { useUpdateLayout } from '../../../backend/use-update-layout';
@ -68,6 +69,7 @@ export function OssFlow() {
const showCreateBlock = useDialogsStore(state => state.showCreateBlock); const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
const showCreateSchema = useDialogsStore(state => state.showCreateSchema); const showCreateSchema = useDialogsStore(state => state.showCreateSchema);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showDeleteReference = useDialogsStore(state => state.showDeleteReference);
const showEditBlock = useDialogsStore(state => state.showEditBlock); const showEditBlock = useDialogsStore(state => state.showEditBlock);
const showImportSchema = useDialogsStore(state => state.showImportSchema); const showImportSchema = useDialogsStore(state => state.showImportSchema);
@ -150,11 +152,22 @@ export function OssFlow() {
if (!canDeleteOperation(item)) { if (!canDeleteOperation(item)) {
return; return;
} }
showDeleteOperation({ switch (item.operation_type) {
oss: schema, case OperationType.REFERENCE:
target: item, showDeleteReference({
layout: getLayout() oss: schema,
}); target: item,
layout: getLayout()
});
break;
case OperationType.INPUT:
case OperationType.SYNTHESIS:
showDeleteOperation({
oss: schema,
target: item,
layout: getLayout()
});
}
} else { } else {
if (!window.confirm(promptText.deleteBlock)) { if (!window.confirm(promptText.deleteBlock)) {
return; return;

View File

@ -16,8 +16,11 @@ export function BlockStats({ target, oss }: BlockStatsProps) {
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length, count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: operations.filter(item => !!item.result).length, count_schemas: operations.filter(item => !!item.result).length,
count_owned: operations.filter(item => !!item.result && !item.is_import).length, count_owned: operations.filter(
count_block: contents.length - operations.length item => !!item.result && (item.operation_type !== OperationType.INPUT || !item.is_import)
).length,
count_block: contents.length - operations.length,
count_references: operations.filter(item => item.operation_type === OperationType.REFERENCE).length
}; };
return <OssStats stats={blockStats} className='pr-3' />; return <OssStats stats={blockStats} className='pr-3' />;

View File

@ -10,6 +10,7 @@ import { useAppLayoutStore, useMainHeight } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { OperationType } from '../../../../backend/types';
import { NodeType } from '../../../../models/oss'; import { NodeType } from '../../../../models/oss';
import { useOssEdit } from '../../oss-edit-context'; import { useOssEdit } from '../../oss-edit-context';
@ -80,7 +81,12 @@ export function SidePanel({ isMounted, className }: SidePanelProps) {
<div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div> <div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div>
) : selectedOperation && selectedSchema && debouncedMounted ? ( ) : selectedOperation && selectedSchema && debouncedMounted ? (
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<ViewSchema schemaID={selectedSchema} isMutable={isMutable && !selectedOperation.is_import} /> <ViewSchema
schemaID={selectedSchema}
isMutable={
isMutable && (selectedOperation.operation_type !== OperationType.INPUT || !selectedOperation.is_import)
}
/>
</Suspense> </Suspense>
) : null} ) : null}
{selectedBlock ? <BlockStats target={selectedBlock} oss={schema} /> : null} {selectedBlock ? <BlockStats target={selectedBlock} oss={schema} /> : null}

View File

@ -227,7 +227,7 @@ export function ToolbarSchema({
/> />
<DropdownButton <DropdownButton
title='Перейти к концептуальной схеме' title='Перейти к концептуальной схеме'
text='перейти к схеме' text='Открыть КС'
icon={<IconRSForm size='1rem' className='icon-primary' />} icon={<IconRSForm size='1rem' className='icon-primary' />}
onClick={navigateRSForm} onClick={navigateRSForm}
/> />

View File

@ -109,7 +109,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
} }
function canDeleteOperation(target: IOperation) { function canDeleteOperation(target: IOperation) {
if (target.operation_type === OperationType.INPUT) { if (target.operation_type === OperationType.INPUT || target.operation_type === OperationType.REFERENCE) {
return true; return true;
} }
return schema.graph.expandOutputs([target.id]).length === 0; return schema.graph.expandOutputs([target.id]).length === 0;

View File

@ -6,6 +6,7 @@ import { TextArea, TextInput } from '@/components/input';
import { CstType, type IUpdateConstituentaDTO } from '../../backend/types'; import { CstType, type IUpdateConstituentaDTO } from '../../backend/types';
import { IconCrucialValue } from '../../components/icon-crucial-value'; import { IconCrucialValue } from '../../components/icon-crucial-value';
import { RSInput } from '../../components/rs-input';
import { SelectCstType } from '../../components/select-cst-type'; import { SelectCstType } from '../../components/select-cst-type';
import { getRSDefinitionPlaceholder, labelCstTypification } from '../../labels'; import { getRSDefinitionPlaceholder, labelCstTypification } from '../../labels';
import { type IConstituenta, type IRSForm } from '../../models/rsform'; import { type IConstituenta, type IRSForm } from '../../models/rsform';
@ -97,9 +98,9 @@ export function FormEditCst({ target, schema }: FormEditCstProps) {
name='item_data.definition_formal' name='item_data.definition_formal'
render={({ field }) => render={({ field }) =>
!!field.value || (!isElementary && !target.is_inherited) ? ( !!field.value || (!isElementary && !target.is_inherited) ? (
<TextArea <RSInput
id='dlg_cst_expression' id='dlg_cst_expression'
fitContent noTooltip
label={ label={
cst_type === CstType.STRUCTURED cst_type === CstType.STRUCTURED
? 'Область определения' ? 'Область определения'
@ -109,9 +110,9 @@ export function FormEditCst({ target, schema }: FormEditCstProps) {
} }
placeholder={getRSDefinitionPlaceholder(cst_type)} placeholder={getRSDefinitionPlaceholder(cst_type)}
className='max-h-15' className='max-h-15'
schema={schema}
value={field.value} value={field.value}
onChange={field.onChange} onChange={field.onChange}
error={errors.item_data?.definition_formal}
disabled={target.is_inherited} disabled={target.is_inherited}
/> />
) : ( ) : (

View File

@ -11,6 +11,7 @@ import { type DlgCreateBlockProps } from '@/features/oss/dialogs/dlg-create-bloc
import { type DlgCreateSchemaProps } from '@/features/oss/dialogs/dlg-create-schema'; import { type DlgCreateSchemaProps } from '@/features/oss/dialogs/dlg-create-schema';
import { type DlgCreateSynthesisProps } from '@/features/oss/dialogs/dlg-create-synthesis/dlg-create-synthesis'; import { type DlgCreateSynthesisProps } from '@/features/oss/dialogs/dlg-create-synthesis/dlg-create-synthesis';
import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation'; import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation';
import { type DlgDeleteReferenceProps } from '@/features/oss/dialogs/dlg-delete-reference';
import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block'; import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block';
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation'; import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
import { type DlgImportSchemaProps } from '@/features/oss/dialogs/dlg-import-schema'; import { type DlgImportSchemaProps } from '@/features/oss/dialogs/dlg-import-schema';
@ -47,31 +48,32 @@ export const DialogType = {
CREATE_SYNTHESIS: 9, CREATE_SYNTHESIS: 9,
EDIT_OPERATION: 10, EDIT_OPERATION: 10,
DELETE_OPERATION: 11, DELETE_OPERATION: 11,
CHANGE_INPUT_SCHEMA: 12, DELETE_REFERENCE: 12,
RELOCATE_CONSTITUENTS: 13, CHANGE_INPUT_SCHEMA: 13,
OSS_SETTINGS: 14, RELOCATE_CONSTITUENTS: 14,
EDIT_CONSTITUENTA: 15, OSS_SETTINGS: 15,
EDIT_CONSTITUENTA: 16,
CLONE_LIBRARY_ITEM: 16, CLONE_LIBRARY_ITEM: 17,
UPLOAD_RSFORM: 17, UPLOAD_RSFORM: 18,
EDIT_EDITORS: 18, EDIT_EDITORS: 19,
EDIT_VERSIONS: 19, EDIT_VERSIONS: 20,
CHANGE_LOCATION: 20, CHANGE_LOCATION: 21,
EDIT_REFERENCE: 21, EDIT_REFERENCE: 22,
EDIT_WORD_FORMS: 22, EDIT_WORD_FORMS: 23,
INLINE_SYNTHESIS: 23, INLINE_SYNTHESIS: 24,
SHOW_QR_CODE: 24, SHOW_QR_CODE: 25,
SHOW_AST: 25, SHOW_AST: 26,
SHOW_TYPE_GRAPH: 26, SHOW_TYPE_GRAPH: 27,
GRAPH_PARAMETERS: 27, GRAPH_PARAMETERS: 28,
SHOW_TERM_GRAPH: 28, SHOW_TERM_GRAPH: 29,
CREATE_SCHEMA: 29, CREATE_SCHEMA: 30,
IMPORT_SCHEMA: 30, IMPORT_SCHEMA: 31,
AI_PROMPT: 31, AI_PROMPT: 32,
CREATE_PROMPT_TEMPLATE: 32 CREATE_PROMPT_TEMPLATE: 33
} as const; } as const;
export type DialogType = (typeof DialogType)[keyof typeof DialogType]; export type DialogType = (typeof DialogType)[keyof typeof DialogType];
@ -104,6 +106,7 @@ interface DialogsStore {
showCloneLibraryItem: (props: DlgCloneLibraryItemProps) => void; showCloneLibraryItem: (props: DlgCloneLibraryItemProps) => void;
showCreateVersion: (props: DlgCreateVersionProps) => void; showCreateVersion: (props: DlgCreateVersionProps) => void;
showDeleteOperation: (props: DlgDeleteOperationProps) => void; showDeleteOperation: (props: DlgDeleteOperationProps) => void;
showDeleteReference: (props: DlgDeleteReferenceProps) => void;
showGraphParams: () => void; showGraphParams: () => void;
showOssOptions: () => void; showOssOptions: () => void;
showRelocateConstituents: (props: DlgRelocateConstituentsProps) => void; showRelocateConstituents: (props: DlgRelocateConstituentsProps) => void;
@ -148,6 +151,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }), showCloneLibraryItem: props => set({ active: DialogType.CLONE_LIBRARY_ITEM, props: props }),
showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, props: props }), showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, props: props }),
showDeleteOperation: props => set({ active: DialogType.DELETE_OPERATION, props: props }), showDeleteOperation: props => set({ active: DialogType.DELETE_OPERATION, props: props }),
showDeleteReference: props => set({ active: DialogType.DELETE_REFERENCE, props: props }),
showGraphParams: () => set({ active: DialogType.GRAPH_PARAMETERS, props: null }), showGraphParams: () => set({ active: DialogType.GRAPH_PARAMETERS, props: null }),
showOssOptions: () => set({ active: DialogType.OSS_SETTINGS, props: null }), showOssOptions: () => set({ active: DialogType.OSS_SETTINGS, props: null }),
showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }), showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }),

View File

@ -165,7 +165,8 @@
} }
.react-flow__node-input, .react-flow__node-input,
.react-flow__node-synthesis { .react-flow__node-synthesis,
.react-flow__node-reference {
border-radius: 5px; border-radius: 5px;
border-width: 0; border-width: 0;