F: Implementing references pt1

This commit is contained in:
Ivan 2025-08-04 15:59:16 +03:00
parent f78ee7c705
commit 13e56f51ea
38 changed files with 775 additions and 119 deletions

View File

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

View File

@ -23,6 +23,7 @@ from apps.rsform.models import (
from .Argument import Argument
from .Inheritance import Inheritance
from .Operation import Operation, OperationType
from .Reference import Reference
from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]]
@ -36,13 +37,15 @@ class OperationSchemaCached:
self.model = model
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. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
if keep_connections:
referred_operations = target.getQ_reference_target()
referred_operations = operation.getQ_reference_target()
if len(referred_operations) == 1:
referred_operation = referred_operations[0]
for arg in target.getQ_as_argument():
for arg in operation.getQ_as_argument():
arg.pk = None
arg.argument = referred_operation
arg.save()
@ -50,7 +53,9 @@ class OperationSchemaCached:
pass
# if target.result_id is not None:
# self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema
target.delete()
self.cache.remove_operation(target)
operation.delete()
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. '''
@ -707,17 +712,23 @@ class OssCache:
self._schemas: list[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.graph = Graph[int]()
for operation in self.operations:
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 \
.filter(operation__oss=self._oss.model) \
.only('operation_id', 'argument_id') \
.order_by('order')
for argument in arguments:
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.substitutions: dict[int, list[Substitution]] = {}
@ -785,18 +796,12 @@ class OssCache:
schema.cache.ensure_loaded()
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:
''' Insert new argument. '''
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:
''' Insert new inheritance. '''
@ -839,6 +844,10 @@ class OssCache:
def remove_argument(self, argument: Argument) -> None:
''' Remove argument from cache. '''
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:
''' Remove substitution from cache. '''

View File

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

View File

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

View File

@ -1,5 +1,7 @@
''' Tests for Django Models. '''
from .t_Argument import *
from .t_Inheritance import *
from .t_Layout import *
from .t_Operation import *
from .t_Reference 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_constituents import *
from .t_operations import *
from .t_references import *
from .t_substitutions import *

View File

@ -0,0 +1,199 @@
''' 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.ks1X1
}])
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 S1 D1 D2 D3',
convention='KS5D4'
)
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}
]
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)
# @decl_endpoint('/api/oss/{item}/delete-reference', method='patch')
# def test_delete_reference_propagation(self):
# ''' Test propagation when deleting a reference operation. '''
# schema_cached = OperationSchemaCached(self.schema2)
# # Ensure reference exists
# self.assertIn(self.reference.pk, schema_cached.cache.operation_by_id)
# # Delete the reference
# schema_cached.delete_reference(self.reference.pk)
# # Reference should be deleted
# with self.assertRaises(Reference.DoesNotExist):
# Reference.objects.get(pk=self.reference.pk)
# # Operation2 should still exist
# self.assertTrue(Operation.objects.filter(pk=self.op2.pk).exists())
# @decl_endpoint('/api/oss/{item}/set-arguments', method='patch')
# def test_set_arguments_propagation(self):
# ''' Test propagation when setting arguments for a reference. '''
# schema_cached = OperationSchemaCached(self.schema2)
# # Add op1 as argument to op2
# schema_cached.set_arguments(self.op2.pk, [self.op1])
# op2 = Operation.objects.get(pk=self.op2.pk)
# args = list(op2.getQ_arguments())
# self.assertEqual(len(args), 1)
# self.assertEqual(args[0].argument, self.op1)
# @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
# def test_delete_operation_with_reference(self):
# ''' Test propagation when deleting an operation that is referenced. '''
# schema_cached = OperationSchemaCached(self.schema1)
# # op1 is referenced by reference
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
# # Delete op1
# schema_cached.delete_operation(self.op1.pk)
# # op1 should be deleted
# with self.assertRaises(Operation.DoesNotExist):
# Operation.objects.get(pk=self.op1.pk)
# # Reference should be deleted as well
# self.assertFalse(Reference.objects.filter(pk=self.reference.pk).exists())
# @decl_endpoint('/api/oss/{item}/add-constituent', method='patch')
# def test_add_constituent_propagation(self):
# ''' Test propagation when adding a constituent to a referenced schema. '''
# # Add a new constituent to schema1 (referenced by op1, which is referenced by reference)
# new_cst = Constituenta.objects.create(
# schema=self.schema1, alias='cst_new', title='New Constituenta', cst_type=CstType.ATTRIBUTE
# )
# # Simulate propagation: after adding, the reference should still be valid and schema1 should have the new cst
# self.assertTrue(Constituenta.objects.filter(pk=new_cst.pk, schema=self.schema1).exists())
# # The reference's target operation's result should include the new constituent
# op1 = Operation.objects.get(pk=self.op1.pk)
# self.assertEqual(op1.result, self.schema1)
# self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
# @decl_endpoint('/api/oss/{item}/remove-constituent', method='patch')
# def test_remove_constituent_propagation(self):
# ''' Test propagation when removing a constituent from a referenced schema. '''
# # Remove cst2 from schema1
# self.cst2.delete()
# # The reference's target operation's result should not include cst2
# op1 = Operation.objects.get(pk=self.op1.pk)
# self.assertEqual(op1.result, self.schema1)
# self.assertNotIn(self.cst2, Constituenta.objects.filter(schema=op1.result))
# # Reference should still be valid
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
# @decl_endpoint('/api/oss/{item}/add-constituent-to-referenced-schema', method='patch')
# def test_propagation_to_multiple_references(self):
# ''' Test propagation when a schema is referenced by multiple references and constituents are added. '''
# # Create another reference to op1
# reference2 = Reference.objects.create(
# alias='ref2', title='Reference 2', type=OperationType.REFERENCE, result=self.schema2
# )
# reference2.setQ_reference_target(self.op1)
# reference2.save()
# # Add a new constituent to schema1
# new_cst = Constituenta.objects.create(
# schema=self.schema1, alias='cst_multi', title='Multi Constituenta', cst_type=CstType.ATTRIBUTE
# )
# # Both references should still be valid and op1's result should include the new constituent
# op1 = Operation.objects.get(pk=self.op1.pk)
# self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
# self.assertEqual(self.reference.getQ_reference_target(), self.op1)
# self.assertEqual(reference2.getQ_reference_target(), self.op1)

View File

@ -683,7 +683,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic():
oss = m.OperationSchemaCached(item)
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'])
return Response(

View File

@ -45,6 +45,11 @@ const DlgDeleteOperation = React.lazy(() =>
default: module.DlgDeleteOperation
}))
);
const DlgDeleteReference = React.lazy(() =>
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
default: module.DlgDeleteReference
}))
);
const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
default: module.DlgEditEditors
@ -196,6 +201,8 @@ export const GlobalDialogs = () => {
return <DlgCreateVersion />;
case DialogType.DELETE_OPERATION:
return <DlgDeleteOperation />;
case DialogType.DELETE_REFERENCE:
return <DlgDeleteReference />;
case DialogType.GRAPH_PARAMETERS:
return <DlgGraphParams />;
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 { TbHexagonLetterP as IconCstPredicate } 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 { LuDna as IconTerminology } from 'react-icons/lu';
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 { LuPower as IconKeepAliasOn } from 'react-icons/lu';
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
export { VscReferences as IconPhantom } from 'react-icons/vsc';
// ===== Domain actions =====
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 { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
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 { LuWandSparkles as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr';

View File

@ -9,10 +9,12 @@ import {
type ICloneSchemaDTO,
type IConstituentaReference,
type ICreateBlockDTO,
type ICreateReferenceDTO,
type ICreateSchemaDTO,
type ICreateSynthesisDTO,
type IDeleteBlockDTO,
type IDeleteOperationDTO,
type IDeleteReferenceDTO,
type IImportSchemaDTO,
type IInputCreatedResponse,
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 }) =>
axiosPost<ICreateSchemaDTO, IOperationCreatedResponse>({
schema: schemaOperationCreatedResponse,

View File

@ -29,7 +29,7 @@ export class OssLoader {
private schemaIDs: number[] = [];
constructor(input: RO<IOperationSchemaDTO>) {
this.oss = structuredClone(input) as IOperationSchema;
this.oss = structuredClone(input) as unknown as IOperationSchema;
}
produceOSS(): IOperationSchema {
@ -83,16 +83,37 @@ export class OssLoader {
}
private inferOperationAttributes() {
const referenceCounts = new Map<number, number>();
this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!;
const position = this.oss.layout.find(item => item.nodeID === operation.nodeID);
operation.x = position?.x ?? 0;
operation.y = position?.y ?? 0;
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);
switch (operation.operation_type) {
case OperationType.INPUT:
break;
case OperationType.SYNTHESIS:
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.oss.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;
}
});
}
@ -126,8 +147,11 @@ export class OssLoader {
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemaIDs.length,
count_owned: operations.filter(item => !!item.result && !item.is_import).length,
count_block: this.oss.blocks.length
count_owned: operations.filter(
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. */
export const OperationType = {
INPUT: 'input',
SYNTHESIS: 'synthesis'
SYNTHESIS: 'synthesis',
REFERENCE: 'reference'
} as const;
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. */
export type ICreateSchemaDTO = z.infer<typeof schemaCreateSchema>;
export type ICreateReferenceDTO = z.infer<typeof schemaCreateReference>;
export type ICreateSynthesisDTO = z.infer<typeof schemaCreateSynthesis>;
export type IImportSchemaDTO = z.infer<typeof schemaImportSchema>;
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. */
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}. */
export interface ITargetOperation {
layout: IOssLayout;
@ -119,6 +124,11 @@ export const schemaBlock = z.strictObject({
parent: z.number().nullable()
});
const schemaReference = z.strictObject({
target: z.number(),
reference: z.number()
});
export const schemaPosition = z.strictObject({
x: z.number(),
y: z.number(),
@ -148,6 +158,7 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(),
operations: z.array(schemaOperation),
blocks: z.array(schemaBlock),
references: z.array(schemaReference),
layout: schemaOssLayout,
arguments: z
.object({
@ -188,6 +199,12 @@ export const schemaCreateSchema = z.strictObject({
position: schemaPosition
});
export const schemaCreateReference = z.strictObject({
target: z.number(),
layout: schemaOssLayout,
position: schemaPosition
});
export const schemaCloneSchema = z.strictObject({
layout: schemaOssLayout,
source_operation: z.number(),
@ -230,6 +247,13 @@ export const schemaDeleteOperation = z.strictObject({
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({
layout: schemaOssLayout,
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>
<b>Тип:</b> {labelOperationType(operation.operation_type)}
</p>
{operation.is_import ? (
{operation.operation_type === OperationType.INPUT && operation.is_import ? (
<p>
<b>КС не принадлежит ОСС</b>
</p>
) : null}
{operation.is_consolidation ? (
{operation.operation_type === OperationType.SYNTHESIS && operation.is_consolidation ? (
<p>
<b>Ромбовидный синтез</b>
</p>
@ -69,7 +69,7 @@ export function InfoOperation({ operation }: InfoOperationProps) {
{operation.description}
</p>
) : null}
{operation.substitutions.length > 0 ? (
{operation.operation_type === OperationType.SYNTHESIS && operation.substitutions.length > 0 ? (
<DataTable
dense
noHeader

View File

@ -1,6 +1,7 @@
import {
IconConceptBlock,
IconDownload,
IconReference,
IconRSForm,
IconRSFormImported,
IconRSFormOwned,
@ -18,14 +19,16 @@ interface OssStatsProps {
export function OssStats({ className, stats }: OssStatsProps) {
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 '>
<span>Всего</span>
<span>{stats.count_all}</span>
</div>
<ValueStats id='count_block' title='Блоки' icon={<IconConceptBlock size='1.25rem' />} value={stats.count_block} />
<ValueStats
id='count_inputs'
className='col-start-1'
title='Загрузка'
icon={<IconDownload size='1.25rem' />}
value={stats.count_inputs}
@ -36,6 +39,12 @@ export function OssStats({ className, stats }: OssStatsProps) {
icon={<IconSynthesis size='1.25rem' />}
value={stats.count_synthesis}
/>
<ValueStats
id='count_references'
title='Синтез'
icon={<IconReference size='1.25rem' />}
value={stats.count_references}
/>
<ValueStats
id='count_schemas'

View File

@ -9,13 +9,13 @@ import { Checkbox, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal';
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 { type IOperation, type IOperationSchema } from '../models/oss';
import { type IOperationInput, type IOperationSchema, type IOperationSynthesis } from '../models/oss';
export interface DlgDeleteOperationProps {
oss: IOperationSchema;
target: IOperation;
target: IOperationInput | IOperationSynthesis;
layout: IOssLayout;
}
@ -54,13 +54,13 @@ export function DlgDeleteOperation() {
<Checkbox
label='Удалить схему'
titleHtml={
target.is_import || target.result === null
(target.operation_type === OperationType.INPUT && target.is_import) || target.result === null
? 'Привязанную схему нельзя удалить'
: 'Удалить схему вместе с операцией'
}
value={field.value}
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,78 @@
'use client';
import { Controller, useForm } 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
}
});
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_constituents'
render={({ field }) => (
<Checkbox
label='Сохранить наследованные конституенты'
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
value={field.value}
onChange={field.onChange}
disabled={target.result === null}
/>
)}
/>
<Controller
control={control}
name='keep_connections'
render={({ field }) => (
<Checkbox
label='Переадресовать связи на оригинал'
titleHtml='Связи аргументов будут перенаправлены на оригинал ссылки'
value={field.value}
onChange={field.onChange}
disabled={target.result === null}
/>
)}
/>
</ModalForm>
);
}

View File

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

View File

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

View File

@ -4,7 +4,8 @@ import {
type ICreateSynthesisDTO,
type IImportSchemaDTO,
type INodePosition,
type IOssLayout
type IOssLayout,
OperationType
} from '../backend/types';
import { type IOperationSchema, NodeType } from './oss';
@ -258,7 +259,11 @@ export class LayoutManager {
}
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);
let inputsPositions = this.layout.filter(pos => freeInputs.includes(pos.nodeID));
if (inputsPositions.length === 0) {

View File

@ -8,7 +8,8 @@ import {
type IBlockDTO,
type ICstSubstituteInfo,
type IOperationDTO,
type IOperationSchemaDTO
type IOperationSchemaDTO,
type OperationType
} from '../backend/types';
/** Represents OSS node type. */
@ -18,26 +19,50 @@ export const NodeType = {
} as const;
export type NodeType = (typeof NodeType)[keyof typeof NodeType];
/** Represents Operation. */
export interface IOperation extends IOperationDTO {
/** Represents OSS graph node. */
export interface IOssNode {
nodeID: string;
nodeType: typeof NodeType.OPERATION;
nodeType: NodeType;
parent: number | null;
x: 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;
}
/** 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'
substitutions: ICstSubstituteInfo[];
arguments: number[];
}
/** Represents Operation. */
export type IOperation = IOperationInput | IOperationReference | IOperationSynthesis;
/** Represents Block. */
export interface IBlock extends IBlockDTO {
nodeID: string;
export interface IBlock extends IOssNode, IBlockDTO {
nodeType: typeof NodeType.BLOCK;
x: number;
y: number;
width: number;
height: number;
}
/** Represents item of OperationSchema. */
@ -51,10 +76,11 @@ export interface IOperationSchemaStats {
count_schemas: number;
count_owned: number;
count_block: number;
count_references: number;
}
/** Represents OperationSchema. */
export interface IOperationSchema extends IOperationSchemaDTO {
export interface IOperationSchema extends Omit<IOperationSchemaDTO, 'operations'> {
operations: IOperation[];
blocks: IBlock[];

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'}
>
{!!item ? (
item.nodeType === NodeType.OPERATION ? (
<MenuOperation operation={item} onHide={onHide} />
) : (
item.nodeType === NodeType.BLOCK ? (
<MenuBlock block={item} onHide={onHide} />
) : (
<MenuOperation operation={item} onHide={onHide} />
)
) : null}
</Dropdown>

View File

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

View File

@ -22,6 +22,8 @@ interface NodeCoreProps {
export function NodeCore({ node }: NodeCoreProps) {
const { selectedItems, schema } = useOssEdit();
const opType = node.data.operation.operation_type;
const focus = selectedItems.length === 1 ? selectedItems[0] : null;
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(
'cc-node-operation h-[40px] w-[150px]',
'relative flex items-center justify-center p-[2px]',
opType === OperationType.REFERENCE && 'border-dashed',
isChild && 'border-accent-orange'
)}
>
@ -45,7 +48,7 @@ export function NodeCore({ node }: NodeCoreProps) {
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
icon={<IconRSForm className={hasFile ? 'text-constructive' : 'text-destructive'} size='12px' />}
/>
{node.data.operation.is_consolidation ? (
{opType === OperationType.SYNTHESIS && node.data.operation.is_consolidation ? (
<Indicator
noPadding
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
@ -66,11 +69,11 @@ export function NodeCore({ node }: NodeCoreProps) {
</div>
) : 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]' />
) : 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]' />
) : null}

View File

@ -2,10 +2,12 @@ import { type NodeTypes } from 'reactflow';
import { BlockNode } from './block-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 = {
synthesis: OperationNode,
input: InputNode,
synthesis: SynthesisNode,
reference: ReferenceNode,
block: BlockNode
} 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';
export function OperationNode(node: OperationInternalNode) {
export function SynthesisNode(node: OperationInternalNode) {
return (
<>
<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 { withPreventDefault } from '@/utils/utils';
import { OperationType } from '../../../backend/types';
import { useDeleteBlock } from '../../../backend/use-delete-block';
import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdateLayout } from '../../../backend/use-update-layout';
@ -68,6 +69,7 @@ export function OssFlow() {
const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
const showCreateSchema = useDialogsStore(state => state.showCreateSchema);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showDeleteReference = useDialogsStore(state => state.showDeleteReference);
const showEditBlock = useDialogsStore(state => state.showEditBlock);
const showImportSchema = useDialogsStore(state => state.showImportSchema);
@ -150,11 +152,22 @@ export function OssFlow() {
if (!canDeleteOperation(item)) {
return;
}
showDeleteOperation({
oss: schema,
target: item,
layout: getLayout()
});
switch (item.operation_type) {
case OperationType.REFERENCE:
showDeleteReference({
oss: schema,
target: item,
layout: getLayout()
});
break;
case OperationType.INPUT:
case OperationType.SYNTHESIS:
showDeleteOperation({
oss: schema,
target: item,
layout: getLayout()
});
}
} else {
if (!window.confirm(promptText.deleteBlock)) {
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_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: operations.filter(item => !!item.result).length,
count_owned: operations.filter(item => !!item.result && !item.is_import).length,
count_block: contents.length - operations.length
count_owned: operations.filter(
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' />;

View File

@ -10,6 +10,7 @@ import { useAppLayoutStore, useMainHeight } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants';
import { OperationType } from '../../../../backend/types';
import { NodeType } from '../../../../models/oss';
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>
) : selectedOperation && selectedSchema && debouncedMounted ? (
<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>
) : null}
{selectedBlock ? <BlockStats target={selectedBlock} oss={schema} /> : null}

View File

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

View File

@ -6,6 +6,7 @@ import { TextArea, TextInput } from '@/components/input';
import { CstType, type IUpdateConstituentaDTO } from '../../backend/types';
import { IconCrucialValue } from '../../components/icon-crucial-value';
import { RSInput } from '../../components/rs-input';
import { SelectCstType } from '../../components/select-cst-type';
import { getRSDefinitionPlaceholder, labelCstTypification } from '../../labels';
import { type IConstituenta, type IRSForm } from '../../models/rsform';
@ -97,9 +98,9 @@ export function FormEditCst({ target, schema }: FormEditCstProps) {
name='item_data.definition_formal'
render={({ field }) =>
!!field.value || (!isElementary && !target.is_inherited) ? (
<TextArea
<RSInput
id='dlg_cst_expression'
fitContent
noTooltip
label={
cst_type === CstType.STRUCTURED
? 'Область определения'
@ -109,9 +110,9 @@ export function FormEditCst({ target, schema }: FormEditCstProps) {
}
placeholder={getRSDefinitionPlaceholder(cst_type)}
className='max-h-15'
schema={schema}
value={field.value}
onChange={field.onChange}
error={errors.item_data?.definition_formal}
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 DlgCreateSynthesisProps } from '@/features/oss/dialogs/dlg-create-synthesis/dlg-create-synthesis';
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 DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
import { type DlgImportSchemaProps } from '@/features/oss/dialogs/dlg-import-schema';
@ -47,31 +48,32 @@ export const DialogType = {
CREATE_SYNTHESIS: 9,
EDIT_OPERATION: 10,
DELETE_OPERATION: 11,
CHANGE_INPUT_SCHEMA: 12,
RELOCATE_CONSTITUENTS: 13,
OSS_SETTINGS: 14,
EDIT_CONSTITUENTA: 15,
DELETE_REFERENCE: 12,
CHANGE_INPUT_SCHEMA: 13,
RELOCATE_CONSTITUENTS: 14,
OSS_SETTINGS: 15,
EDIT_CONSTITUENTA: 16,
CLONE_LIBRARY_ITEM: 16,
UPLOAD_RSFORM: 17,
EDIT_EDITORS: 18,
EDIT_VERSIONS: 19,
CHANGE_LOCATION: 20,
CLONE_LIBRARY_ITEM: 17,
UPLOAD_RSFORM: 18,
EDIT_EDITORS: 19,
EDIT_VERSIONS: 20,
CHANGE_LOCATION: 21,
EDIT_REFERENCE: 21,
EDIT_WORD_FORMS: 22,
INLINE_SYNTHESIS: 23,
EDIT_REFERENCE: 22,
EDIT_WORD_FORMS: 23,
INLINE_SYNTHESIS: 24,
SHOW_QR_CODE: 24,
SHOW_AST: 25,
SHOW_TYPE_GRAPH: 26,
GRAPH_PARAMETERS: 27,
SHOW_TERM_GRAPH: 28,
CREATE_SCHEMA: 29,
IMPORT_SCHEMA: 30,
SHOW_QR_CODE: 25,
SHOW_AST: 26,
SHOW_TYPE_GRAPH: 27,
GRAPH_PARAMETERS: 28,
SHOW_TERM_GRAPH: 29,
CREATE_SCHEMA: 30,
IMPORT_SCHEMA: 31,
AI_PROMPT: 31,
CREATE_PROMPT_TEMPLATE: 32
AI_PROMPT: 32,
CREATE_PROMPT_TEMPLATE: 33
} as const;
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
@ -104,6 +106,7 @@ interface DialogsStore {
showCloneLibraryItem: (props: DlgCloneLibraryItemProps) => void;
showCreateVersion: (props: DlgCreateVersionProps) => void;
showDeleteOperation: (props: DlgDeleteOperationProps) => void;
showDeleteReference: (props: DlgDeleteReferenceProps) => void;
showGraphParams: () => void;
showOssOptions: () => 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 }),
showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, 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 }),
showOssOptions: () => set({ active: DialogType.OSS_SETTINGS, props: null }),
showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }),

View File

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