From 266fdf0c304ff43ff1a07ce2bde3ab1b184a5ce3 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 4 Aug 2025 22:42:02 +0300 Subject: [PATCH] F: Implementing references pt2 --- .../apps/oss/models/OperationSchemaCached.py | 31 ++--- .../oss/tests/s_propagation/t_references.py | 129 ++++++------------ .../src/features/oss/backend/oss-loader.ts | 17 ++- .../dlg-create-synthesis/tab-arguments.tsx | 5 +- .../oss/dialogs/dlg-delete-reference.tsx | 29 ++-- .../dlg-edit-operation/tab-arguments.tsx | 9 +- .../frontend/src/features/oss/models/oss.ts | 1 + .../context-menu/menu-operation.tsx | 31 ++++- .../oss/pages/oss-page/oss-edit-state.tsx | 2 +- 9 files changed, 117 insertions(+), 137 deletions(-) diff --git a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py index 08d57fd4..b22752db 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py +++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py @@ -39,34 +39,29 @@ class OperationSchemaCached: def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False): ''' Delete Reference Operation. ''' + if not keep_connections: + self.delete_operation(target, keep_constituents) + return self.cache.ensure_loaded_subs() operation = self.cache.operation_by_id[target] - if keep_connections: - referred_operations = operation.getQ_reference_target() - if len(referred_operations) == 1: - referred_operation = referred_operations[0] - for arg in operation.getQ_as_argument(): - arg.pk = None - arg.argument = referred_operation - arg.save() - else: - pass - # if target.result_id is not None: - # self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema - + reference_target = self.cache.reference_target.get(target) + if reference_target: + for arg in operation.getQ_as_argument(): + arg.argument_id = reference_target + arg.save() self.cache.remove_operation(target) operation.delete() + def delete_operation(self, target: int, keep_constituents: bool = False): ''' Delete Operation. ''' self.cache.ensure_loaded_subs() operation = self.cache.operation_by_id[target] - schema = self.cache.get_schema(operation) children = self.cache.graph.outputs[target] - if schema is not None and len(children) > 0: - ids = [cst.pk for cst in schema.cache.constituents] + if operation.result is not None and len(children) > 0: + ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True)) if not keep_constituents: - self.before_delete_cst(schema.model.pk, ids) + self._cascade_delete_inherited(operation.pk, ids) else: inheritance_to_delete: list[Inheritance] = [] for child_id in children: @@ -837,6 +832,8 @@ class OssCache: del self._schema_by_id[target.result_id] self.operations.remove(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: del self.substitutions[operation] del self.inheritance[operation] diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py index e4992579..6bb37062 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py @@ -80,23 +80,42 @@ class ReferencePropagationTestCase(EndpointTester): self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3]) self.owned.set_substitutions(self.operation5.pk, [{ 'original': self.ks4X1, - 'substitution': self.ks1X1 + '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 S1 D1 D2 D3', + 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.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 @@ -106,94 +125,22 @@ class ReferencePropagationTestCase(EndpointTester): 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) - # @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) +# TODO: add more tests diff --git a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts index d0aad68c..d90a0fee 100644 --- a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts +++ b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts @@ -27,6 +27,7 @@ export class OssLoader { private itemByNodeID = new Map(); private blockByID = new Map(); private schemaIDs: number[] = []; + private extendedGraph = new Graph(); constructor(input: RO) { this.oss = structuredClone(input) as unknown as IOperationSchema; @@ -47,6 +48,7 @@ export class OssLoader { result.hierarchy = this.hierarchy; result.schemas = this.schemaIDs; result.stats = this.calculateStats(); + result.extendedGraph = this.extendedGraph; return result; } @@ -57,6 +59,7 @@ export class OssLoader { this.itemByNodeID.set(operation.nodeID, operation); this.operationByID.set(operation.id, operation); this.graph.addNode(operation.id); + this.extendedGraph.addNode(operation.id); this.hierarchy.addNode(operation.nodeID); if (operation.parent) { this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID); @@ -75,7 +78,13 @@ export class OssLoader { } 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() { @@ -102,7 +111,7 @@ export class OssLoader { break; case OperationType.REFERENCE: const ref = this.oss.references.find(item => item.reference === operationID); - const target = !!ref ? this.oss.operationByID.get(ref.target) : null; + const target = !!ref ? this.operationByID.get(ref.target) : null; if (!target || !ref) { throw new Error(`Reference ${operationID} not found`); } @@ -128,13 +137,13 @@ export class OssLoader { } private inferConsolidation(operationID: number): boolean { - const inputs = this.graph.expandInputs([operationID]); + const inputs = this.extendedGraph.expandInputs([operationID]); if (inputs.length === 0) { return false; } const ancestors = [...inputs]; inputs.forEach(input => { - ancestors.push(...this.graph.expandAllInputs([input])); + ancestors.push(...this.extendedGraph.expandAllInputs([input])); }); const unique = new Set(ancestors); return unique.size < ancestors.length; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-synthesis/tab-arguments.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-synthesis/tab-arguments.tsx index f662b0f6..19e3238f 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-synthesis/tab-arguments.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-synthesis/tab-arguments.tsx @@ -18,6 +18,9 @@ export function TabArguments() { } = useFormContext(); 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 (
( - + )} />
diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-reference.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-reference.tsx index 7c028fc5..3b7ab864 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-reference.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-reference.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Controller, useForm } from 'react-hook-form'; +import { Controller, useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { HelpTopic } from '@/features/help'; @@ -32,6 +32,7 @@ export function DlgDeleteReference() { keep_connections: false } }); + const keep_connections = useWatch({ control, name: 'keep_connections' }); function onSubmit(data: IDeleteReferenceDTO) { return deleteReference({ itemID: oss.id, data: data }); @@ -47,19 +48,6 @@ export function DlgDeleteReference() { helpTopic={HelpTopic.CC_PROPAGATION} > - ( - - )} - /> )} /> + ( + + )} + /> ); } diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx index a5edb2f3..e344b2f1 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Label } from '@/components/input'; import { useDialogsStore } from '@/stores/dialogs'; @@ -12,7 +12,12 @@ import { type DlgEditOperationProps } from './dlg-edit-operation'; export function TabArguments() { const { control, setValue } = useFormContext(); 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)); function handleChangeArguments(prev: number[], newValue: number[]) { diff --git a/rsconcept/frontend/src/features/oss/models/oss.ts b/rsconcept/frontend/src/features/oss/models/oss.ts index 18310514..c17c57b2 100644 --- a/rsconcept/frontend/src/features/oss/models/oss.ts +++ b/rsconcept/frontend/src/features/oss/models/oss.ts @@ -85,6 +85,7 @@ export interface IOperationSchema extends Omit; schemas: number[]; stats: IOperationSchemaStats; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx index b3fbe1e6..465dfaf3 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx @@ -206,15 +206,32 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) { }).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 ( <> - } - onClick={handleEditOperation} - disabled={!isMutable || isProcessing} - /> + {operation.operation_type !== OperationType.REFERENCE ? ( + } + onClick={handleEditOperation} + disabled={!isMutable || isProcessing} + /> + ) : ( + } + onClick={handleSelectTarget} + /> + )} {operation.result ? (