Compare commits

...

8 Commits

Author SHA1 Message Date
Ivan
fbd84ece4d R: Invalidate OSS on RSForm change. Add lazy loading
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-08-16 12:44:09 +03:00
Ivan
f59206c182 F: Implement operation and schema delete consequence for OSS 2024-08-15 23:23:10 +03:00
Ivan
4716fde331 B: Prevent oss cross-propagation 2024-08-14 23:36:52 +03:00
Ivan
e17056ea10 F: Prepare operation change propagation 2024-08-14 21:50:10 +03:00
Ivan
63991f713c R: Merge ChangeManager and OperationSchema 2024-08-14 13:13:00 +03:00
Ivan
4d3f91dd5e Update rsforms.py 2024-08-13 23:53:36 +03:00
Ivan
e484ad2663 Update dependencies 2024-08-13 23:44:41 +03:00
Ivan
6d3d07dbc0 M: Implement substitution updates on cst_delete 2024-08-13 23:37:32 +03:00
52 changed files with 1702 additions and 878 deletions

View File

@ -123,3 +123,11 @@ class LibraryItem(Model):
def versions(self) -> QuerySet[Version]: def versions(self) -> QuerySet[Version]:
''' Get all Versions of this item. ''' ''' Get all Versions of this item. '''
return Version.objects.filter(item=self.pk).order_by('-time_create') return Version.objects.filter(item=self.pk).order_by('-time_create')
def is_synced(self, target: 'LibraryItem') -> bool:
''' Check if item is synced with target. '''
if self.owner != target.owner:
return False
if self.location != target.location:
return False
return True

View File

@ -217,13 +217,10 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}', method='delete') @decl_endpoint('/api/library/{item}', method='delete')
def test_destroy(self): def test_destroy(self):
response = self.execute(item=self.owned.pk) self.executeNoContent(item=self.owned.pk)
self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
self.executeForbidden(item=self.unowned.pk) self.executeForbidden(item=self.unowned.pk)
self.toggle_admin(True) self.toggle_admin(True)
response = self.execute(item=self.unowned.pk) self.executeNoContent(item=self.unowned.pk)
self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
@decl_endpoint('/api/library/active', method='get') @decl_endpoint('/api/library/active', method='get')

View File

@ -13,7 +13,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.oss.models import Operation, OperationSchema from apps.oss.models import Operation, OperationSchema, PropagationFacade
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User from apps.users.models import User
@ -67,6 +67,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
if update_list: if update_list:
Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment']) Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment'])
def perform_destroy(self, instance: m.LibraryItem) -> None:
PropagationFacade.before_delete_schema(instance)
return super().perform_destroy(instance)
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'partial_update']: if self.action in ['update', 'partial_update']:
access_level = permissions.ItemEditor access_level = permissions.ItemEditor

View File

@ -1,400 +0,0 @@
''' Models: Change propagation manager. '''
from typing import Optional, cast
from cctext import extract_entities
from rest_framework.serializers import ValidationError
from apps.library.models import LibraryItem
from apps.rsform.graph import Graph
from apps.rsform.models import (
INSERT_LAST,
Constituenta,
CstType,
RSForm,
extract_globals,
replace_entities,
replace_globals
)
from .Inheritance import Inheritance
from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .Substitution import Substitution
CstMapping = dict[str, Constituenta]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class ChangeManager:
''' Change propagation wrapper for OSS. '''
def __init__(self, model: LibraryItem):
self.oss = OperationSchema(model)
self.cache = OssCache(self.oss)
def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. '''
self.cache.insert(source)
inserted_aliases = [cst.alias for cst in cst_list]
depend_aliases: set[str] = set()
for new_cst in cst_list:
depend_aliases.update(new_cst.extract_references())
depend_aliases.difference_update(inserted_aliases)
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
operation = self.cache.get_operation(source)
self._cascade_create_cst(cst_list, operation, alias_mapping)
def after_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta type is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
self._cascade_change_cst_type(target.pk, target.cst_type, operation)
def after_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
depend_aliases = self._extract_data_references(data, old_data)
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
self._cascade_update_cst(target.pk, operation, data, old_data, alias_mapping)
def before_delete(self, target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
self._cascade_before_delete(target, operation)
def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source)
self._cascade_before_substitute(substitutions, operation)
def _cascade_before_substitute(
self,
substitutions: CstSubstitution,
operation: Operation
) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
child_schema.cache.ensure_loaded()
new_substitutions = self._transform_substitutions(substitutions, child_operation, child_schema)
if len(new_substitutions) == 0:
continue
self._cascade_before_substitute(new_substitutions, child_operation)
child_schema.substitute(new_substitutions)
def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
source_schema = self.cache.get_schema(operation)
assert source_schema is not None
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
# TODO: update substitutions for diamond synthesis (if needed)
self.cache.ensure_loaded()
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
insert_where = self._determine_insert_position(cst_list[0], child_operation, source_schema, child_schema)
new_cst_list = child_schema.insert_copy(cst_list, insert_where, alias_mapping)
for index, cst in enumerate(new_cst_list):
new_inheritance = Inheritance.objects.create(
operation=child_operation,
child=cst,
parent=cst_list[index]
)
self.cache.insert_inheritance(new_inheritance)
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_create_cst(new_cst_list, child_operation, new_mapping)
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype):
self._cascade_change_cst_type(successor_id, ctype, child_operation)
# pylint: disable=too-many-arguments
def _cascade_update_cst(
self,
cst_id: int, operation: Operation,
data: dict, old_data: dict,
mapping: CstMapping
) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
assert child_schema is not None
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
successor = child_schema.cache.by_id.get(successor_id)
if successor is None:
continue
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
if len(new_data) == 0:
continue
new_old_data = child_schema.update_cst(successor, new_data)
if len(new_old_data) == 0:
continue
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_update_cst(successor_id, child_operation, new_data, new_old_data, new_mapping)
def _cascade_before_delete(self, target: list[Constituenta], operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
child_schema.cache.ensure_loaded()
# TODO: check if substitutions are affected. Undo substitutions before deletion
child_target_cst = []
child_target_ids = []
for cst in target:
successor_id = self.cache.get_inheritor(cst.pk, child_id)
if successor_id is not None:
child_target_ids.append(successor_id)
child_target_cst.append(child_schema.cache.by_id[successor_id])
self._cascade_before_delete(child_target_cst, child_operation)
self.cache.remove_cst(child_target_ids, child_id)
child_schema.delete_cst(child_target_cst)
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
if len(mapping) == 0:
return mapping
result: CstMapping = {}
for alias, cst in mapping.items():
successor_id = self.cache.get_successor(cst.pk, operation.pk)
if successor_id is None:
continue
successor = schema.cache.by_id.get(successor_id)
if successor is None:
continue
result[alias] = successor
return result
def _determine_insert_position(
self, prototype: Constituenta,
operation: Operation,
source: RSForm,
destination: RSForm
) -> int:
''' Determine insert_after for new constituenta. '''
if prototype.order == 1:
return 1
prev_cst = source.cache.constituents[prototype.order - 2]
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
if inherited_prev_id is None:
return INSERT_LAST
prev_cst = destination.cache.by_id[inherited_prev_id]
return cast(int, prev_cst.order) + 1
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
result: set[str] = set()
if 'definition_formal' in data:
result.update(extract_globals(data['definition_formal']))
result.update(extract_globals(old_data['definition_formal']))
if 'term_raw' in data:
result.update(extract_entities(data['term_raw']))
result.update(extract_entities(old_data['term_raw']))
if 'definition_raw' in data:
result.update(extract_entities(data['definition_raw']))
result.update(extract_entities(old_data['definition_raw']))
return result
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
new_data = {}
if 'term_forms' in data:
if old_data['term_forms'] == cst.term_forms:
new_data['term_forms'] = data['term_forms']
if 'convention' in data:
if old_data['convention'] == cst.convention:
new_data['convention'] = data['convention']
if 'definition_formal' in data:
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
if 'term_raw' in data:
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
if 'definition_raw' in data:
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
return new_data
def _transform_substitutions(
self,
target: CstSubstitution,
operation: Operation,
schema: RSForm
) -> CstSubstitution:
result: CstSubstitution = []
for current_sub in target:
sub_replaced = False
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation.pk)
if new_substitution_id is None:
for sub in self.cache.substitutions[operation.pk]:
if sub.original_id == current_sub[1].pk:
sub_replaced = True
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation.pk)
break
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation.pk)
original_replaced = False
if new_original_id is None:
for sub in self.cache.substitutions[operation.pk]:
if sub.original_id == current_sub[0].pk:
original_replaced = True
sub.original_id = current_sub[1].pk
sub.save()
new_original_id = new_substitution_id
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation.pk)
break
if sub_replaced and original_replaced:
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
for sub in self.cache.substitutions[operation.pk]:
if sub.substitution_id == current_sub[0].pk:
sub.substitution_id = current_sub[1].pk
sub.save()
if new_original_id is not None and new_substitution_id is not None:
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
return result
class OssCache:
''' Cache for OSS data. '''
def __init__(self, oss: OperationSchema):
self._oss = oss
self._schemas: list[RSForm] = []
self._schema_by_id: dict[int, RSForm] = {}
self.operations = list(oss.operations().only('result_id'))
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)
for argument in self._oss.arguments().only('operation_id', 'argument_id'):
self.graph.add_edge(argument.argument_id, argument.operation_id)
self.is_loaded = False
self.substitutions: dict[int, list[Substitution]] = {}
self.inheritance: dict[int, list[Inheritance]] = {}
def insert(self, schema: RSForm) -> None:
''' Insert new schema. '''
if not self._schema_by_id.get(schema.model.pk):
self._insert_new(schema)
def get_schema(self, operation: Operation) -> Optional[RSForm]:
''' Get schema by Operation. '''
if operation.result_id is None:
return None
if operation.result_id in self._schema_by_id:
return self._schema_by_id[operation.result_id]
else:
schema = RSForm.from_id(operation.result_id)
schema.cache.ensure_loaded()
self._insert_new(schema)
return schema
def get_operation(self, schema: RSForm) -> Operation:
''' Get operation by schema. '''
for operation in self.operations:
if operation.result_id == schema.model.pk:
return operation
raise ValueError(f'Operation for schema {schema.model.pk} not found')
def ensure_loaded(self) -> None:
''' Ensure propagation of changes. '''
if self.is_loaded:
return
self.is_loaded = True
for operation in self.operations:
if operation.operation_type != OperationType.INPUT:
self.inheritance[operation.pk] = []
self.substitutions[operation.pk] = []
for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'):
self.substitutions[sub.operation_id].append(sub)
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
self.inheritance[item.operation_id].append(item)
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom. '''
for item in self.inheritance[operation]:
if item.parent_id == parent_cst:
return item.child_id
return None
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom including substitutions. '''
for sub in self.substitutions[operation]:
if sub.original_id == parent_cst:
return self.get_inheritor(sub.substitution_id, operation)
return self.get_inheritor(parent_cst, operation)
def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append(inheritance)
def insert_substitution(self, sub: Substitution) -> None:
''' Insert new inheritance. '''
self.substitutions[sub.operation_id].append(sub)
def remove_cst(self, target: list[int], operation: int) -> None:
''' Remove constituents from operation. '''
subs_to_delete = [
sub for sub in self.substitutions[operation]
if sub.original_id in target or sub.substitution_id in target
]
for sub in subs_to_delete:
self.substitutions[operation].remove(sub)
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
for item in inherit_to_delete:
self.inheritance[operation].remove(item)
def _insert_new(self, schema: RSForm) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema

View File

@ -1,22 +1,38 @@
''' Models: OSS API. ''' ''' Models: OSS API. '''
from typing import Optional from typing import Optional, cast
from cctext import extract_entities
from django.db.models import QuerySet from django.db.models import QuerySet
from rest_framework.serializers import ValidationError
from apps.library.models import Editor, LibraryItem, LibraryItemType from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.rsform.models import Constituenta, RSForm from apps.rsform.graph import Graph
from apps.rsform.models import (
DELETED_ALIAS,
INSERT_LAST,
Constituenta,
CstType,
RSForm,
extract_globals,
replace_entities,
replace_globals
)
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation from .Operation import Operation
from .Substitution import Substitution from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class OperationSchema: class OperationSchema:
''' Operations schema API. ''' ''' Operations schema API. '''
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem):
self.model = model self.model = model
self.cache = OssCache(self)
@staticmethod @staticmethod
def create(**kwargs) -> 'OperationSchema': def create(**kwargs) -> 'OperationSchema':
@ -75,34 +91,45 @@ class OperationSchema:
def create_operation(self, **kwargs) -> Operation: def create_operation(self, **kwargs) -> Operation:
''' Insert new operation. ''' ''' Insert new operation. '''
result = Operation.objects.create(oss=self.model, **kwargs) result = Operation.objects.create(oss=self.model, **kwargs)
self.save() self.cache.insert_operation(result)
result.refresh_from_db() self.save(update_fields=['time_update'])
return result return result
def delete_operation(self, operation: Operation): def delete_operation(self, target: Operation, keep_constituents: bool = False):
''' Delete operation. ''' ''' Delete operation. '''
operation.delete() if not keep_constituents:
schema = self.cache.get_schema(target)
if schema is not None:
self.before_delete_cst(schema.cache.constituents, schema)
self.cache.remove_operation(target.pk)
target.delete()
self.save(update_fields=['time_update'])
# TODO: deal with attached schema def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
# TODO: trigger on_change effects
self.save()
def set_input(self, target: Operation, schema: Optional[LibraryItem]) -> None:
''' Set input schema for operation. ''' ''' Set input schema for operation. '''
if schema == target.result: operation = self.cache.operation_by_id[target]
has_children = len(self.cache.graph.outputs[target]) > 0
old_schema = self.cache.get_schema(operation)
if schema == old_schema:
return return
target.result = schema
if old_schema is not None:
if has_children:
self.before_delete_cst(old_schema.cache.constituents, old_schema)
self.cache.remove_schema(old_schema)
operation.result = schema
if schema is not None: if schema is not None:
target.result = schema operation.result = schema
target.alias = schema.alias operation.alias = schema.alias
target.title = schema.title operation.title = schema.title
target.comment = schema.comment operation.comment = schema.comment
target.save() operation.save(update_fields=['result', 'alias', 'title', 'comment'])
# TODO: trigger on_change effects if schema is not None and has_children:
rsform = RSForm(schema)
self.save() self.after_create_cst(list(rsform.constituents()), rsform)
self.save(update_fields=['time_update'])
def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None: def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None:
''' Set arguments to operation. ''' ''' Set arguments to operation. '''
@ -122,7 +149,7 @@ class OperationSchema:
if not changed: if not changed:
return return
# TODO: trigger on_change effects # TODO: trigger on_change effects
self.save() self.save(update_fields=['time_update'])
def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None: def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None:
''' Clear all arguments for operation. ''' ''' Clear all arguments for operation. '''
@ -153,7 +180,7 @@ class OperationSchema:
return return
# TODO: trigger on_change effects # TODO: trigger on_change effects
self.save() self.save(update_fields=['time_update'])
def create_input(self, operation: Operation) -> RSForm: def create_input(self, operation: Operation) -> RSForm:
''' Create input RSForm. ''' ''' Create input RSForm. '''
@ -169,7 +196,7 @@ class OperationSchema:
Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True)) Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True))
operation.result = schema.model operation.result = schema.model
operation.save() operation.save()
self.save() self.save(update_fields=['time_update'])
return schema return schema
def execute_operation(self, operation: Operation) -> bool: def execute_operation(self, operation: Operation) -> bool:
@ -210,5 +237,504 @@ class OperationSchema:
receiver.restore_order() receiver.restore_order()
receiver.reset_aliases() receiver.reset_aliases()
self.save() self.save(update_fields=['time_update'])
return True return True
def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. '''
self.cache.insert(source)
inserted_aliases = [cst.alias for cst in cst_list]
depend_aliases: set[str] = set()
for new_cst in cst_list:
depend_aliases.update(new_cst.extract_references())
depend_aliases.difference_update(inserted_aliases)
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
operation = self.cache.get_operation(source.model.pk)
self._cascade_create_cst(cst_list, operation, alias_mapping)
def after_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta type is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_change_cst_type(target.pk, target.cst_type, operation.pk)
def after_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. '''
self.cache.insert(source)
operation = self.cache.get_operation(source.model.pk)
depend_aliases = self._extract_data_references(data, old_data)
alias_mapping: CstMapping = {}
for alias in depend_aliases:
cst = source.cache.by_alias.get(alias)
if cst is not None:
alias_mapping[alias] = cst
self._cascade_update_cst(
cst_id=target.pk,
operation=operation.pk,
data=data,
old_data=old_data,
mapping=alias_mapping
)
def before_delete_cst(self, target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_before_delete(target, operation.pk)
def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
self.cache.insert(source)
operation = self.cache.get_operation(source.model.pk)
self._cascade_before_substitute(substitutions, operation)
def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
source_schema = self.cache.get_schema(operation)
assert source_schema is not None
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
# TODO: update substitutions for diamond synthesis (if needed)
self.cache.ensure_loaded()
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = OperationSchema._produce_alias_mapping(new_mapping)
insert_where = self._determine_insert_position(cst_list[0], child_operation, source_schema, child_schema)
new_cst_list = child_schema.insert_copy(cst_list, insert_where, alias_mapping)
for index, cst in enumerate(new_cst_list):
new_inheritance = Inheritance.objects.create(
operation=child_operation,
child=cst,
parent=cst_list[index]
)
self.cache.insert_inheritance(new_inheritance)
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_create_cst(new_cst_list, child_operation, new_mapping)
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: int) -> None:
children = self.cache.graph.outputs[operation]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype):
self._cascade_change_cst_type(successor_id, ctype, child_id)
# pylint: disable=too-many-arguments
def _cascade_update_cst(
self,
cst_id: int, operation: int,
data: dict, old_data: dict,
mapping: CstMapping
) -> None:
children = self.cache.graph.outputs[operation]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
successor_id = self.cache.get_inheritor(cst_id, child_id)
if successor_id is None:
continue
child_schema = self.cache.get_schema(child_operation)
assert child_schema is not None
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
alias_mapping = OperationSchema._produce_alias_mapping(new_mapping)
successor = child_schema.cache.by_id.get(successor_id)
if successor is None:
continue
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
if len(new_data) == 0:
continue
new_old_data = child_schema.update_cst(successor, new_data)
if len(new_old_data) == 0:
continue
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_update_cst(
cst_id=successor_id,
operation=child_id,
data=new_data,
old_data=new_old_data,
mapping=new_mapping
)
def _cascade_before_delete(self, target: list[Constituenta], operation: int) -> None:
children = self.cache.graph.outputs[operation]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
self._undo_substitutions_cst(target, child_operation, child_schema)
child_target_ids = self.cache.get_inheritors_list([cst.pk for cst in target], child_id)
child_target_cst = [child_schema.cache.by_id[cst_id] for cst_id in child_target_ids]
self._cascade_before_delete(child_target_cst, child_id)
if len(child_target_cst) > 0:
self.cache.remove_cst(child_target_ids, child_id)
child_schema.delete_cst(child_target_cst)
def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
if len(new_substitutions) == 0:
continue
self._cascade_before_substitute(new_substitutions, child_operation)
child_schema.substitute(new_substitutions)
def _cascade_partial_mapping(
self,
mapping: CstMapping,
target: list[int],
operation: Operation,
schema: RSForm
) -> None:
alias_mapping = OperationSchema._produce_alias_mapping(mapping)
schema.apply_partial_mapping(alias_mapping, target)
children = self.cache.graph.outputs[operation.pk]
if len(children) == 0:
return
self.cache.ensure_loaded()
for child_id in children:
child_operation = self.cache.operation_by_id[child_id]
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
if not new_mapping:
continue
new_target = self.cache.get_inheritors_list(target, child_id)
if len(new_target) == 0:
continue
self._cascade_partial_mapping(new_mapping, new_target, child_operation, child_schema)
@staticmethod
def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]:
result: dict[str, str] = {}
for alias, cst in mapping.items():
if cst is None:
result[alias] = DELETED_ALIAS
else:
result[alias] = cst.alias
return result
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
if len(mapping) == 0:
return mapping
result: CstMapping = {}
for alias, cst in mapping.items():
if cst is None:
result[alias] = None
continue
successor_id = self.cache.get_successor(cst.pk, operation.pk)
if successor_id is None:
continue
successor = schema.cache.by_id.get(successor_id)
if successor is None:
continue
result[alias] = successor
return result
def _determine_insert_position(
self, prototype: Constituenta,
operation: Operation,
source: RSForm,
destination: RSForm
) -> int:
''' Determine insert_after for new constituenta. '''
if prototype.order == 1:
return 1
prev_cst = source.cache.constituents[prototype.order - 2]
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
if inherited_prev_id is None:
return INSERT_LAST
prev_cst = destination.cache.by_id[inherited_prev_id]
return cast(int, prev_cst.order) + 1
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
result: set[str] = set()
if 'definition_formal' in data:
result.update(extract_globals(data['definition_formal']))
result.update(extract_globals(old_data['definition_formal']))
if 'term_raw' in data:
result.update(extract_entities(data['term_raw']))
result.update(extract_entities(old_data['term_raw']))
if 'definition_raw' in data:
result.update(extract_entities(data['definition_raw']))
result.update(extract_entities(old_data['definition_raw']))
return result
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
new_data = {}
if 'term_forms' in data:
if old_data['term_forms'] == cst.term_forms:
new_data['term_forms'] = data['term_forms']
if 'convention' in data:
if old_data['convention'] == cst.convention:
new_data['convention'] = data['convention']
if 'definition_formal' in data:
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
if 'term_raw' in data:
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
if 'definition_raw' in data:
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
return new_data
def _transform_substitutions(
self,
target: CstSubstitution,
operation: int,
schema: RSForm
) -> CstSubstitution:
result: CstSubstitution = []
for current_sub in target:
sub_replaced = False
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation)
if new_substitution_id is None:
for sub in self.cache.substitutions[operation]:
if sub.original_id == current_sub[1].pk:
sub_replaced = True
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation)
break
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation)
original_replaced = False
if new_original_id is None:
for sub in self.cache.substitutions[operation]:
if sub.original_id == current_sub[0].pk:
original_replaced = True
sub.original_id = current_sub[1].pk
sub.save()
new_original_id = new_substitution_id
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation)
break
if sub_replaced and original_replaced:
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
for sub in self.cache.substitutions[operation]:
if sub.substitution_id == current_sub[0].pk:
sub.substitution_id = current_sub[1].pk
sub.save()
if new_original_id is not None and new_substitution_id is not None:
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
return result
def _undo_substitutions_cst(self, target: list[Constituenta], operation: Operation, schema: RSForm) -> None:
target_ids = [cst.pk for cst in target]
to_process = []
for sub in self.cache.substitutions[operation.pk]:
if sub.original_id in target_ids or sub.substitution_id in target_ids:
to_process.append(sub)
for sub in to_process:
self._undo_substitution(sub, schema, target_ids)
def _undo_substitution(self, target: Substitution, schema: RSForm, ignore_parents: list[int]) -> None:
operation = self.cache.operation_by_id[target.operation_id]
original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target)
dependant = []
for cst_id in original_schema.get_dependant([original_cst.pk]):
if cst_id not in ignore_parents:
inheritor_id = self.cache.get_inheritor(cst_id, operation.pk)
if inheritor_id is not None:
dependant.append(inheritor_id)
self.cache.substitutions[operation.pk].remove(target)
target.delete()
new_original: Optional[Constituenta] = None
if original_cst.pk not in ignore_parents:
full_cst = Constituenta.objects.get(pk=original_cst.pk)
self.after_create_cst([full_cst], original_schema)
new_original_id = self.cache.get_inheritor(original_cst.pk, operation.pk)
assert new_original_id is not None
new_original = schema.cache.by_id[new_original_id]
if len(dependant) == 0:
return
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation.pk)
assert substitution_id is not None
substitution_inheritor = schema.cache.by_id[substitution_id]
mapping = {cast(str, substitution_inheritor.alias): new_original}
self._cascade_partial_mapping(mapping, dependant, operation, schema)
class OssCache:
''' Cache for OSS data. '''
def __init__(self, oss: OperationSchema):
self._oss = oss
self._schemas: list[RSForm] = []
self._schema_by_id: dict[int, RSForm] = {}
self.operations = list(oss.operations().only('result_id'))
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)
for argument in self._oss.arguments().only('operation_id', 'argument_id'):
self.graph.add_edge(argument.argument_id, argument.operation_id)
self.is_loaded = False
self.substitutions: dict[int, list[Substitution]] = {}
self.inheritance: dict[int, list[Inheritance]] = {}
def insert(self, schema: RSForm) -> None:
''' Insert new schema. '''
if not self._schema_by_id.get(schema.model.pk):
schema.cache.ensure_loaded()
self._insert_new(schema)
def get_schema(self, operation: Operation) -> Optional[RSForm]:
''' Get schema by Operation. '''
if operation.result_id is None:
return None
if operation.result_id in self._schema_by_id:
return self._schema_by_id[operation.result_id]
else:
schema = RSForm.from_id(operation.result_id)
schema.cache.ensure_loaded()
self._insert_new(schema)
return schema
def get_operation(self, schema: int) -> Operation:
''' Get operation by schema. '''
for operation in self.operations:
if operation.result_id == schema:
return operation
raise ValueError(f'Operation for schema {schema} not found')
def ensure_loaded(self) -> None:
''' Ensure cache is fully loaded. '''
if self.is_loaded:
return
self.is_loaded = True
for operation in self.operations:
self.inheritance[operation.pk] = []
self.substitutions[operation.pk] = []
for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'):
self.substitutions[sub.operation_id].append(sub)
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
self.inheritance[item.operation_id].append(item)
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom. '''
for item in self.inheritance[operation]:
if item.parent_id == parent_cst:
return item.child_id
return None
def get_inheritors_list(self, target: list[int], operation: int) -> list[int]:
''' Get child for parent inside target RSFrom. '''
result = []
for item in self.inheritance[operation]:
if item.parent_id in target:
result.append(item.child_id)
return result
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom including substitutions. '''
for sub in self.substitutions[operation]:
if sub.original_id == parent_cst:
return self.get_inheritor(sub.substitution_id, operation)
return self.get_inheritor(parent_cst, operation)
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:
self.substitutions[operation.pk] = []
self.inheritance[operation.pk] = []
def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append(inheritance)
def insert_substitution(self, sub: Substitution) -> None:
''' Insert new substitution. '''
self.substitutions[sub.operation_id].append(sub)
def remove_cst(self, target: list[int], operation: int) -> None:
''' Remove constituents from operation. '''
subs_to_delete = [
sub for sub in self.substitutions[operation]
if sub.original_id in target or sub.substitution_id in target
]
for sub in subs_to_delete:
self.substitutions[operation].remove(sub)
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
for item in inherit_to_delete:
self.inheritance[operation].remove(item)
def remove_operation(self, operation: int) -> None:
''' Remove operation from cache. '''
target = self.operation_by_id[operation]
if target.result_id in self._schema_by_id:
self._schemas.remove(self._schema_by_id[target.result_id])
del self._schema_by_id[target.result_id]
self.operations.remove(self.operation_by_id[operation])
del self.operation_by_id[operation]
if self.is_loaded:
del self.substitutions[operation]
del self.inheritance[operation]
def remove_schema(self, schema: RSForm) -> None:
''' Remove schema from cache. '''
self._schemas.remove(schema)
del self._schema_by_id[schema.model.pk]
def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
operation = self.operation_by_id[sub.operation_id]
parents = self.graph.inputs[operation.pk]
original_cst = None
substitution_cst = None
original_schema = None
substitution_schema = None
for parent_id in parents:
parent_schema = self.get_schema(self.operation_by_id[parent_id])
if parent_schema is None:
continue
if sub.original_id in parent_schema.cache.by_id:
original_schema = parent_schema
original_cst = original_schema.cache.by_id[sub.original_id]
if sub.substitution_id in parent_schema.cache.by_id:
substitution_schema = parent_schema
substitution_cst = substitution_schema.cache.by_id[sub.substitution_id]
if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None:
raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.')
return original_schema, substitution_schema, original_cst, substitution_cst
def _insert_new(self, schema: RSForm) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema

View File

@ -1,8 +1,8 @@
''' Models: Change propagation facade - managing all changes in OSS. ''' ''' Models: Change propagation facade - managing all changes in OSS. '''
from apps.library.models import LibraryItem from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSForm
from .ChangeManager import ChangeManager from .OperationSchema import CstSubstitution, OperationSchema
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]: def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
@ -13,37 +13,49 @@ def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
class PropagationFacade: class PropagationFacade:
''' Change propagation API. ''' ''' Change propagation API. '''
@classmethod @staticmethod
def after_create_cst(cls, new_cst: list[Constituenta], source: RSForm) -> None: def after_create_cst(new_cst: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. ''' ''' Trigger cascade resolutions when new constituent is created. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).after_create_cst(new_cst, source) OperationSchema(host).after_create_cst(new_cst, source)
@classmethod @staticmethod
def after_change_cst_type(cls, target: Constituenta, source: RSForm) -> None: def after_change_cst_type(target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta type is changed. ''' ''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).after_change_cst_type(target, source) OperationSchema(host).after_change_cst_type(target, source)
@classmethod @staticmethod
def after_update_cst(cls, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None: def after_update_cst(target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. ''' ''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).after_update_cst(target, data, old_data, source) OperationSchema(host).after_update_cst(target, data, old_data, source)
@classmethod @staticmethod
def before_delete(cls, target: list[Constituenta], source: RSForm) -> None: def before_delete_cst(target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. ''' ''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).before_delete(target, source) OperationSchema(host).before_delete_cst(target, source)
@classmethod @staticmethod
def before_substitute(cls, substitutions: list[tuple[Constituenta, Constituenta]], source: RSForm) -> None: def before_substitute(substitutions: CstSubstitution, source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. ''' ''' Trigger cascade resolutions before constituents are substituted. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
ChangeManager(host).before_substitute(substitutions, source) OperationSchema(host).before_substitute(substitutions, source)
@staticmethod
def before_delete_schema(item: LibraryItem) -> None:
''' Trigger cascade resolutions before schema is deleted. '''
if item.item_type != LibraryItemType.RSFORM:
return
hosts = _get_oss_hosts(item)
if len(hosts) == 0:
return
schema = RSForm(item)
PropagationFacade.before_delete_cst(list(schema.constituents()), schema)

View File

@ -1,9 +1,8 @@
''' Django: Models. ''' ''' Django: Models. '''
from .Argument import Argument from .Argument import Argument
from .ChangeManager import ChangeManager
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .Substitution import Substitution
from .PropagationFacade import PropagationFacade from .PropagationFacade import PropagationFacade
from .Substitution import Substitution

View File

@ -4,6 +4,7 @@ from .basics import OperationPositionSerializer, PositionsSerializer, Substituti
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,
OperationTargetSerializer, OperationTargetSerializer,

View File

@ -138,6 +138,26 @@ class OperationTargetSerializer(serializers.Serializer):
return attrs return attrs
class OperationDeleteSerializer(serializers.Serializer):
''' Serializer: Delete operation. '''
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
keep_constituents = serializers.BooleanField(default=False, required=False)
delete_schema = serializers.BooleanField(default=False, required=False)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target'])
if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title)
})
return attrs
class SetOperationInputSerializer(serializers.Serializer): class SetOperationInputSerializer(serializers.Serializer):
''' Serializer: Set input schema for operation. ''' ''' Serializer: Set input schema for operation. '''
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())

View File

@ -1,4 +1,5 @@
''' Tests for REST API OSS propagation. ''' ''' Tests for REST API OSS propagation. '''
from .t_attributes import * from .t_attributes import *
from .t_constituents import * from .t_constituents import *
from .t_operations import *
from .t_substitutions import * from .t_substitutions import *

View File

@ -0,0 +1,251 @@
''' Testing API: Change substitutions 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 TestChangeOperations(EndpointTester):
''' Testing Operations change propagation 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_new('X1', convention='KS1X1')
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2')
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1')
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1')
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2')
self.ks2S1 = self.ks2.insert_new(
alias='S1',
definition_formal=r'X1',
convention='KS2S1'
)
self.ks3 = RSForm.create(
alias='KS3',
title='Test3',
owner=self.user
)
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1')
self.ks3D1 = self.ks3.insert_new(
alias='D1',
definition_formal='X1 X1',
convention='KS3D1'
)
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_operation(
alias='3',
operation_type=OperationType.INPUT,
result=self.ks3.model
)
self.operation4 = self.owned.create_operation(
alias='4',
operation_type=OperationType.SYNTHESIS
)
self.owned.set_arguments(self.operation4, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation4, [{
'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_new(
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, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5, [{
'original': self.ks4X1,
'substitution': self.ks3X1
}])
self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db()
self.ks5 = RSForm(self.operation5.result)
self.ks5D4 = self.ks5.insert_new(
alias='D4',
definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
convention='KS5D4'
)
def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3)
self.assertEqual(self.ks3.constituents().count(), 2)
self.assertEqual(self.ks4.constituents().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_input_operation(self):
data = {
'positions': [],
'target': self.operation2.pk
}
self.executeOK(data=data, item=self.owned_id)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 6)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
data = {
'positions': [],
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
self.operation2.refresh_from_db()
self.assertEqual(self.operation2.result, None)
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 6)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3')
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
ks6 = RSForm.create(
alias='KS6',
title='Test6',
owner=self.user
)
ks6X1 = ks6.insert_new('X1', convention='KS6X1')
ks6X2 = ks6.insert_new('X2', convention='KS6X2')
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
data = {
'positions': [],
'target': self.operation2.pk,
'input': ks6.model.pk
}
self.executeOK(data=data, item=self.owned_id)
ks4Dks6 = Constituenta.objects.get(as_child__parent_id=ks6D1.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
self.operation2.refresh_from_db()
self.assertEqual(self.operation2.result, ks6.model)
self.assertEqual(self.operation2.alias, ks6.model.alias)
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 7)
self.assertEqual(self.ks5.constituents().count(), 9)
self.assertEqual(ks4Dks6.definition_formal, r'X5 X6')
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3')
@decl_endpoint('/api/library/{item}', method='delete')
def test_delete_schema(self):
self.executeNoContent(item=self.ks1.model.pk)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 0)
self.assertEqual(self.ks4.constituents().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 DEL D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_and_constituents(self):
data = {
'positions': [],
'target': self.operation1.pk,
'keep_constituents': False,
'delete_schema': True
}
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 0)
self.assertEqual(self.ks4.constituents().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 DEL D3')
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation_keep_constituents(self):
data = {
'positions': [],
'target': self.operation1.pk,
'keep_constituents': True,
'delete_schema': True
}
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')

View File

@ -158,3 +158,33 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(self.ks4D1.definition_formal, r'X2 X1') self.assertEqual(self.ks4D1.definition_formal, r'X2 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1') self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X2 D1 D2 D3') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 X2 D1 D2 D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_original(self):
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks5.constituents().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 DEL D3')
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_substitution(self):
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getSubstitutions()
self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks5.constituents().count(), 7)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 DEL DEL D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 DEL DEL D1 D2 D3')

View File

@ -201,6 +201,9 @@ class TestOssViewset(EndpointTester):
def test_create_operation_result(self): def test_create_operation_result(self):
self.populateData() self.populateData()
self.operation1.result = None
self.operation1.save()
data = { data = {
'item_data': { 'item_data': {
'alias': 'Test4', 'alias': 'Test4',
@ -223,11 +226,14 @@ class TestOssViewset(EndpointTester):
'alias': 'Test4', 'alias': 'Test4',
'title': 'Test title', 'title': 'Test title',
'comment': 'Comment', 'comment': 'Comment',
'operation_type': OperationType.INPUT 'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
}, },
'create_schema': True, 'create_schema': True,
'positions': [], 'positions': [],
} }
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.refresh_from_db()
new_operation = response.data['new_operation'] new_operation = response.data['new_operation']
@ -309,8 +315,6 @@ class TestOssViewset(EndpointTester):
data['target'] = self.operation1.pk data['target'] = self.operation1.pk
data['input'] = None data['input'] = None
data['target'] = self.operation1.pk
self.toggle_admin(True) self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id) self.executeBadData(data=data, item=self.unowned_id)
self.logout() self.logout()
@ -343,9 +347,29 @@ class TestOssViewset(EndpointTester):
'target': self.operation1.pk, 'target': self.operation1.pk,
'input': self.ks2.model.pk 'input': self.ks2.model.pk
} }
response = self.executeOK(data=data, item=self.owned_id) self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
data = {
'positions': [],
'target': self.operation2.pk,
'input': None
}
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db() self.operation2.refresh_from_db()
self.assertEqual(self.operation2.result, self.ks2.model) self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
self.assertEqual(self.ks2.model.visible, True)
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch') @decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self): def test_update_operation(self):

View File

@ -1,7 +1,8 @@
''' Endpoints for OSS. ''' ''' Endpoints for OSS. '''
from typing import cast from typing import Optional, cast
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, serializers from rest_framework import generics, serializers
@ -109,6 +110,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
schema = new_operation.result
if schema is not None:
connected_operations = \
m.Operation.objects \
.filter(Q(result=schema) & ~Q(pk=new_operation.pk)) \
.only('operation_type', 'oss_id')
for operation in connected_operations:
if operation.operation_type != m.OperationType.INPUT:
raise serializers.ValidationError({
'item_data': msg.operationResultFromAnotherOSS()
})
if operation.oss_id == new_operation.oss_id:
raise serializers.ValidationError({
'item_data': msg.operationInputAlreadyConnected()
})
if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']: if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']:
oss.create_input(new_operation) oss.create_input(new_operation)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
@ -127,7 +143,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@extend_schema( @extend_schema(
summary='delete operation', summary='delete operation',
tags=['OSS'], tags=['OSS'],
request=s.OperationTargetSerializer, request=s.OperationDeleteSerializer,
responses={ responses={
c.HTTP_200_OK: s.OperationSchemaSerializer, c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
@ -138,17 +154,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation') @action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk) -> HttpResponse: def delete_operation(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete operation. ''' ''' Endpoint: Delete operation. '''
serializer = s.OperationTargetSerializer( serializer = s.OperationDeleteSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': self.get_object()}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
operation = cast(m.Operation, serializer.validated_data['target'])
old_schema: Optional[LibraryItem] = operation.result
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(serializer.validated_data['target']) oss.delete_operation(operation, serializer.validated_data['keep_constituents'])
if old_schema is not None:
if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema)
old_schema.delete()
elif old_schema.is_synced(oss.model):
old_schema.visible = True
old_schema.save(update_fields=['visible'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data
@ -217,11 +241,28 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) target_operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
schema: Optional[LibraryItem] = serializer.validated_data['input']
if schema is not None:
connected_operations = m.Operation.objects.filter(result=schema).only('operation_type', 'oss_id')
for operation in connected_operations:
if operation.operation_type != m.OperationType.INPUT:
raise serializers.ValidationError({
'input': msg.operationResultFromAnotherOSS()
})
if operation != target_operation and operation.oss_id == target_operation.oss_id:
raise serializers.ValidationError({
'input': msg.operationInputAlreadyConnected()
})
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
old_schema: Optional[LibraryItem] = target_operation.result
with transaction.atomic(): with transaction.atomic():
if old_schema is not None:
if old_schema.is_synced(oss.model):
old_schema.visible = True
old_schema.save(update_fields=['visible'])
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
oss.set_input(operation, serializer.validated_data['input']) oss.set_input(target_operation.pk, schema)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data

View File

@ -29,71 +29,9 @@ DELETED_ALIAS = 'DEL'
class RSForm: class RSForm:
''' RSForm is math form of conceptual schema. ''' ''' RSForm is math form of conceptual schema. '''
class Cache:
''' Cache for RSForm constituents. '''
def __init__(self, schema: 'RSForm'):
self._schema = schema
self.constituents: list[Constituenta] = []
self.by_id: dict[int, Constituenta] = {}
self.by_alias: dict[str, Constituenta] = {}
self.is_loaded = False
def reload(self) -> None:
self.constituents = list(
self._schema.constituents().only(
'order',
'alias',
'cst_type',
'definition_formal',
'term_raw',
'definition_raw'
).order_by('order')
)
self.by_id = {cst.pk: cst for cst in self.constituents}
self.by_alias = {cst.alias: cst for cst in self.constituents}
self.is_loaded = True
def ensure_loaded(self) -> None:
if not self.is_loaded:
self.reload()
def clear(self) -> None:
self.constituents = []
self.by_id = {}
self.by_alias = {}
self.is_loaded = False
def insert(self, cst: Constituenta) -> None:
if self.is_loaded:
self.constituents.insert(cst.order - 1, cst)
self.by_id[cst.pk] = cst
self.by_alias[cst.alias] = cst
def insert_multi(self, items: Iterable[Constituenta]) -> None:
if self.is_loaded:
for cst in items:
self.constituents.insert(cst.order - 1, cst)
self.by_id[cst.pk] = cst
self.by_alias[cst.alias] = cst
def remove(self, target: Constituenta) -> None:
if self.is_loaded:
self.constituents.remove(self.by_id[target.pk])
del self.by_id[target.pk]
del self.by_alias[target.alias]
def remove_multi(self, target: Iterable[Constituenta]) -> None:
if self.is_loaded:
for cst in target:
self.constituents.remove(self.by_id[cst.pk])
del self.by_id[cst.pk]
del self.by_alias[cst.alias]
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem):
self.model = model self.model = model
self.cache: RSForm.Cache = RSForm.Cache(self) self.cache: RSFormCache = RSFormCache(self)
@staticmethod @staticmethod
def create(**kwargs) -> 'RSForm': def create(**kwargs) -> 'RSForm':
@ -107,6 +45,18 @@ class RSForm:
model = LibraryItem.objects.get(pk=pk) model = LibraryItem.objects.get(pk=pk)
return RSForm(model) return RSForm(model)
def get_dependant(self, target: Iterable[int]) -> set[int]:
''' Get list of constituents depending on target (only 1st degree). '''
result: set[int] = set()
terms = self._graph_term()
formal = self._graph_formal()
definitions = self._graph_text()
for cst_id in target:
result.update(formal.outputs[cst_id])
result.update(terms.outputs[cst_id])
result.update(definitions.outputs[cst_id])
return result
def save(self, *args, **kwargs) -> None: def save(self, *args, **kwargs) -> None:
''' Model wrapper. ''' ''' Model wrapper. '''
self.model.save(*args, **kwargs) self.model.save(*args, **kwargs)
@ -138,7 +88,7 @@ class RSForm:
''' Access semantic information on constituents. ''' ''' Access semantic information on constituents. '''
return SemanticInfo(self) return SemanticInfo(self)
def on_term_change(self, changed: list[int]) -> None: def after_term_change(self, changed: list[int]) -> None:
''' Trigger cascade resolutions when term changes. ''' ''' Trigger cascade resolutions when term changes. '''
self.cache.ensure_loaded() self.cache.ensure_loaded()
graph_terms = self._graph_term() graph_terms = self._graph_term()
@ -209,7 +159,7 @@ class RSForm:
result.save() result.save()
self.cache.insert(result) self.cache.insert(result)
self.on_term_change([result.pk]) self.after_term_change([result.pk])
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -239,8 +189,12 @@ class RSForm:
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
return result return result
def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST, def insert_copy(
initial_mapping: Optional[dict[str, str]] = None) -> list[Constituenta]: self,
items: list[Constituenta],
position: int = INSERT_LAST,
initial_mapping: Optional[dict[str, str]] = None
) -> list[Constituenta]:
''' Insert copy of target constituents updating references. ''' ''' Insert copy of target constituents updating references. '''
count = len(items) count = len(items)
if count == 0: if count == 0:
@ -252,10 +206,12 @@ class RSForm:
indices: dict[str, int] = {} indices: dict[str, int] = {}
for (value, _) in CstType.choices: for (value, _) in CstType.choices:
indices[value] = self.get_max_index(cast(CstType, value)) indices[value] = -1
mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {} mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
for cst in items: for cst in items:
if indices[cst.cst_type] == -1:
indices[cst.cst_type] = self.get_max_index(cst.cst_type)
indices[cst.cst_type] = indices[cst.cst_type] + 1 indices[cst.cst_type] = indices[cst.cst_type] + 1
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
mapping[cst.alias] = newAlias mapping[cst.alias] = newAlias
@ -318,7 +274,7 @@ class RSForm:
cst.definition_resolved = resolver.resolve(cst.definition_raw) cst.definition_resolved = resolver.resolve(cst.definition_raw)
cst.save() cst.save()
if term_changed: if term_changed:
self.on_term_change([cst.pk]) self.after_term_change([cst.pk])
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
return old_data return old_data
@ -370,7 +326,7 @@ class RSForm:
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete() Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
self._reset_order() self._reset_order()
self.apply_mapping(mapping) self.apply_mapping(mapping)
self.on_term_change([substitution.pk for substitution in replacements]) self.after_term_change([substitution.pk for substitution in replacements])
def restore_order(self) -> None: def restore_order(self) -> None:
''' Restore order based on types and term graph. ''' ''' Restore order based on types and term graph. '''
@ -382,19 +338,6 @@ class RSForm:
mapping = self._create_reset_mapping() mapping = self._create_reset_mapping()
self.apply_mapping(mapping, change_aliases=True) self.apply_mapping(mapping, change_aliases=True)
def _create_reset_mapping(self) -> dict[str, str]:
bases = cast(dict[str, int], {})
mapping = cast(dict[str, str], {})
for cst_type in CstType.values:
bases[cst_type] = 1
cst_list = self.constituents().order_by('order')
for cst in cst_list:
alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
bases[cst.cst_type] += 1
if cst.alias != alias:
mapping[cst.alias] = alias
return mapping
def change_cst_type(self, target: int, new_type: CstType) -> bool: def change_cst_type(self, target: int, new_type: CstType) -> bool:
''' Change type of constituenta generating alias automatically. ''' ''' Change type of constituenta generating alias automatically. '''
self.cache.ensure_loaded() self.cache.ensure_loaded()
@ -419,6 +362,17 @@ class RSForm:
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw']) Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
def apply_partial_mapping(self, mapping: dict[str, str], target: list[int]) -> None:
''' Apply rename mapping to target constituents. '''
self.cache.ensure_loaded()
update_list: list[Constituenta] = []
for cst in self.cache.constituents:
if cst.pk in target:
if cst.apply_mapping(mapping):
update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['definition_formal', 'term_raw', 'definition_raw'])
self.save(update_fields=['time_update'])
def resolve_all_text(self) -> None: def resolve_all_text(self) -> None:
''' Trigger reference resolution for all texts. ''' ''' Trigger reference resolution for all texts. '''
self.cache.ensure_loaded() self.cache.ensure_loaded()
@ -482,6 +436,19 @@ class RSForm:
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
return result return result
def _create_reset_mapping(self) -> dict[str, str]:
bases = cast(dict[str, int], {})
mapping = cast(dict[str, str], {})
for cst_type in CstType.values:
bases[cst_type] = 1
cst_list = self.constituents().order_by('order')
for cst in cst_list:
alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
bases[cst.cst_type] += 1
if cst.alias != alias:
mapping[cst.alias] = alias
return mapping
def _shift_positions(self, start: int, shift: int) -> None: def _shift_positions(self, start: int, shift: int) -> None:
if shift == 0: if shift == 0:
return return
@ -561,6 +528,68 @@ class RSForm:
return result return result
class RSFormCache:
''' Cache for RSForm constituents. '''
def __init__(self, schema: 'RSForm'):
self._schema = schema
self.constituents: list[Constituenta] = []
self.by_id: dict[int, Constituenta] = {}
self.by_alias: dict[str, Constituenta] = {}
self.is_loaded = False
def reload(self) -> None:
self.constituents = list(
self._schema.constituents().only(
'order',
'alias',
'cst_type',
'definition_formal',
'term_raw',
'definition_raw'
).order_by('order')
)
self.by_id = {cst.pk: cst for cst in self.constituents}
self.by_alias = {cst.alias: cst for cst in self.constituents}
self.is_loaded = True
def ensure_loaded(self) -> None:
if not self.is_loaded:
self.reload()
def clear(self) -> None:
self.constituents = []
self.by_id = {}
self.by_alias = {}
self.is_loaded = False
def insert(self, cst: Constituenta) -> None:
if self.is_loaded:
self.constituents.insert(cst.order - 1, cst)
self.by_id[cst.pk] = cst
self.by_alias[cst.alias] = cst
def insert_multi(self, items: Iterable[Constituenta]) -> None:
if self.is_loaded:
for cst in items:
self.constituents.insert(cst.order - 1, cst)
self.by_id[cst.pk] = cst
self.by_alias[cst.alias] = cst
def remove(self, target: Constituenta) -> None:
if self.is_loaded:
self.constituents.remove(self.by_id[target.pk])
del self.by_id[target.pk]
del self.by_alias[target.alias]
def remove_multi(self, target: Iterable[Constituenta]) -> None:
if self.is_loaded:
for cst in target:
self.constituents.remove(self.by_id[cst.pk])
del self.by_id[cst.pk]
del self.by_alias[cst.alias]
class SemanticInfo: class SemanticInfo:
''' Semantic information derived from constituents. ''' ''' Semantic information derived from constituents. '''

View File

@ -388,7 +388,7 @@ class TestRSForm(DBTester):
x1.term_resolved = 'слон' x1.term_resolved = 'слон'
x1.save() x1.save()
self.schema.on_term_change([x1.pk]) self.schema.after_term_change([x1.pk])
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
x3.refresh_from_db() x3.refresh_from_db()

View File

@ -263,7 +263,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst_list: list[m.Constituenta] = serializer.validated_data['items'] cst_list: list[m.Constituenta] = serializer.validated_data['items']
schema = m.RSForm(model) schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
PropagationFacade.before_delete(cst_list, schema) PropagationFacade.before_delete_cst(cst_list, schema)
schema.delete_cst(cst_list) schema.delete_cst(cst_list)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -594,9 +594,9 @@ def inline_synthesis(request: Request) -> HttpResponse:
index = next(i for (i, cst) in enumerate(items) if cst.pk == replacement.pk) index = next(i for (i, cst) in enumerate(items) if cst.pk == replacement.pk)
replacement = new_items[index] replacement = new_items[index]
substitutions.append((original, replacement)) substitutions.append((original, replacement))
receiver.substitute(substitutions)
# TODO: propagate substitutions PropagationFacade.before_substitute(substitutions, receiver)
receiver.substitute(substitutions)
receiver.restore_order() receiver.restore_order()

View File

@ -1,8 +1,8 @@
tzdata==2024.1 tzdata==2024.1
Django==5.0.7 Django==5.1
djangorestframework==3.15.2 djangorestframework==3.15.2
django-cors-headers==4.4.0 django-cors-headers==4.4.0
django-filter==24.2 django-filter==24.3
drf-spectacular==0.27.2 drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.7.1 drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3 coreapi==2.3.3
@ -11,4 +11,4 @@ cctext==0.1.4
pyconcept==0.1.6 pyconcept==0.1.6
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
gunicorn==22.0.0 gunicorn==23.0.0

View File

@ -30,6 +30,14 @@ def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}' return f'Операция не является Загрузкой: {title}'
def operationResultFromAnotherOSS():
return 'Схема является результатом другой ОСС'
def operationInputAlreadyConnected():
return 'Схема уже подключена к другой операции'
def operationNotSynthesis(title: str): def operationNotSynthesis(title: str):
return f'Операция не является Синтезом: {title}' return f'Операция не является Синтезом: {title}'

View File

@ -12,7 +12,7 @@
"@tanstack/react-table": "^8.20.1", "@tanstack/react-table": "^8.20.1",
"@uiw/codemirror-themes": "^4.23.0", "@uiw/codemirror-themes": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0", "@uiw/react-codemirror": "^4.23.0",
"axios": "^1.7.3", "axios": "^1.7.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.3.24", "framer-motion": "^11.3.24",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
@ -20,7 +20,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-icons": "^5.2.1", "react-icons": "^5.3.0",
"react-intl": "^6.6.8", "react-intl": "^6.6.8",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-pdf": "^9.1.0", "react-pdf": "^9.1.0",
@ -53,7 +53,7 @@
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
"ts-jest": "^29.2.4", "ts-jest": "^29.2.4",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.1", "typescript-eslint": "^8.1.0",
"vite": "^5.4.0" "vite": "^5.4.0"
} }
}, },
@ -357,6 +357,38 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/plugin-syntax-class-static-block": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
"integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz",
"integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-import-meta": { "node_modules/@babel/plugin-syntax-import-meta": {
"version": "7.10.4", "version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
@ -477,6 +509,22 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/plugin-syntax-private-property-in-object": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
"integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-top-level-await": { "node_modules/@babel/plugin-syntax-top-level-await": {
"version": "7.14.5", "version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
@ -700,9 +748,9 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.30.0", "version": "6.32.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.30.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.32.0.tgz",
"integrity": "sha512-96Nmn8OeLh6aONQprIeYk8hGVnEuYpWuxKSkdsODOx9hWPxyuyZGvmvxV/JmLsp+CubMO1PsLaN5TNNgrl0UrQ==", "integrity": "sha512-AgVNvED2QTsZp5e3syoHLsrWtwJFYWdx1Vr/m3f4h1ATQz0ax60CfXF3Htdmk69k2MlYZw8gXesnQdHtzyVmAw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
@ -3606,9 +3654,9 @@
} }
}, },
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.10", "version": "4.4.11",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
@ -3671,17 +3719,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.1.0.tgz",
"integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", "integrity": "sha512-LlNBaHFCEBPHyD4pZXb35mzjGkuGKXU5eeCA1SxvHfiRES0E82dOounfVpL4DCqYvJEKab0bZIA0gCRpdLKkCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/scope-manager": "8.1.0",
"@typescript-eslint/type-utils": "8.0.1", "@typescript-eslint/type-utils": "8.1.0",
"@typescript-eslint/utils": "8.0.1", "@typescript-eslint/utils": "8.1.0",
"@typescript-eslint/visitor-keys": "8.0.1", "@typescript-eslint/visitor-keys": "8.1.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@ -3705,16 +3753,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.1.0.tgz",
"integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", "integrity": "sha512-U7iTAtGgJk6DPX9wIWPPOlt1gO57097G06gIcl0N0EEnNw8RGD62c+2/DiP/zL7KrkqnnqF7gtFGR7YgzPllTA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/scope-manager": "8.1.0",
"@typescript-eslint/types": "8.0.1", "@typescript-eslint/types": "8.1.0",
"@typescript-eslint/typescript-estree": "8.0.1", "@typescript-eslint/typescript-estree": "8.1.0",
"@typescript-eslint/visitor-keys": "8.0.1", "@typescript-eslint/visitor-keys": "8.1.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -3734,14 +3782,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.1.0.tgz",
"integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", "integrity": "sha512-DsuOZQji687sQUjm4N6c9xABJa7fjvfIdjqpSIIVOgaENf2jFXiM9hIBZOL3hb6DHK9Nvd2d7zZnoMLf9e0OtQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.0.1", "@typescript-eslint/types": "8.1.0",
"@typescript-eslint/visitor-keys": "8.0.1" "@typescript-eslint/visitor-keys": "8.1.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3752,14 +3800,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.1.0.tgz",
"integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", "integrity": "sha512-oLYvTxljVvsMnldfl6jIKxTaU7ok7km0KDrwOt1RHYu6nxlhN3TIx8k5Q52L6wR33nOwDgM7VwW1fT1qMNfFIA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.0.1", "@typescript-eslint/typescript-estree": "8.1.0",
"@typescript-eslint/utils": "8.0.1", "@typescript-eslint/utils": "8.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@ -3777,9 +3825,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.1.0.tgz",
"integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", "integrity": "sha512-q2/Bxa0gMOu/2/AKALI0tCKbG2zppccnRIRCW6BaaTlRVaPKft4oVYPp7WOPpcnsgbr0qROAVCVKCvIQ0tbWog==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -3791,14 +3839,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.1.0.tgz",
"integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", "integrity": "sha512-NTHhmufocEkMiAord/g++gWKb0Fr34e9AExBRdqgWdVBaKoei2dIyYKD9Q0jBnvfbEA5zaf8plUFMUH6kQ0vGg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.0.1", "@typescript-eslint/types": "8.1.0",
"@typescript-eslint/visitor-keys": "8.0.1", "@typescript-eslint/visitor-keys": "8.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -3820,16 +3868,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.1.0.tgz",
"integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", "integrity": "sha512-ypRueFNKTIFwqPeJBfeIpxZ895PQhNyH4YID6js0UoBImWYoSjBsahUn9KMiJXh94uOjVBgHD9AmkyPsPnFwJA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/scope-manager": "8.1.0",
"@typescript-eslint/types": "8.0.1", "@typescript-eslint/types": "8.1.0",
"@typescript-eslint/typescript-estree": "8.0.1" "@typescript-eslint/typescript-estree": "8.1.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3843,13 +3891,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.1.0.tgz",
"integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", "integrity": "sha512-ba0lNI19awqZ5ZNKh6wCModMwoZs457StTebQ0q1NP58zSi2F6MOZRXwfKZy+jB78JNJ/WH8GSh2IQNzXX8Nag==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.0.1", "@typescript-eslint/types": "8.1.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@ -4346,9 +4394,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.3", "version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -4547,24 +4595,27 @@
} }
}, },
"node_modules/babel-preset-current-node-syntax": { "node_modules/babel-preset-current-node-syntax": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
"integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-async-generators": "^7.8.4",
"@babel/plugin-syntax-bigint": "^7.8.3", "@babel/plugin-syntax-bigint": "^7.8.3",
"@babel/plugin-syntax-class-properties": "^7.8.3", "@babel/plugin-syntax-class-properties": "^7.12.13",
"@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/plugin-syntax-class-static-block": "^7.14.5",
"@babel/plugin-syntax-import-attributes": "^7.24.7",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.3",
"@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-syntax-numeric-separator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
"@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3",
"@babel/plugin-syntax-top-level-await": "^7.8.3" "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
"@babel/plugin-syntax-top-level-await": "^7.14.5"
}, },
"peerDependencies": { "peerDependencies": {
"@babel/core": "^7.0.0" "@babel/core": "^7.0.0"
@ -5634,9 +5685,9 @@
} }
}, },
"node_modules/detect-gpu": { "node_modules/detect-gpu": {
"version": "5.0.42", "version": "5.0.43",
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.42.tgz", "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.43.tgz",
"integrity": "sha512-Vdhe87ZNhxIS+OGesy9DOx8P3YBbCBapoomGR9kH26HuDAZ6c0FohsrK47j9efL972kLCaD22EbNUYHVLkqx/w==", "integrity": "sha512-KVcUS/YzsZIBIACz6p2xpuBpAjaY4wiELImJ7M8rb9i16NE6frnVpSV/UBpkK6DYj4Wd3NJeE4sghcaypuM8bg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"webgl-constants": "^1.1.1" "webgl-constants": "^1.1.1"
@ -7401,9 +7452,9 @@
} }
}, },
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.1", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -11142,9 +11193,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "6.1.1", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -11340,9 +11391,9 @@
} }
}, },
"node_modules/react-icons": { "node_modules/react-icons": {
"version": "5.2.1", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz",
"integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "*" "react": "*"
@ -12815,9 +12866,9 @@
} }
}, },
"node_modules/three-stdlib": { "node_modules/three-stdlib": {
"version": "2.32.1", "version": "2.32.2",
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.32.1.tgz", "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.32.2.tgz",
"integrity": "sha512-ZgxxLAwtEaKkvfGP+hkW4s6IaDzif47evTdBPwVvdvLsOul3M6l0D4vO4/fzFguXT6FdoBlaTLhteOcn3uDzPg==", "integrity": "sha512-ZN25Na/Xg7APhGKwJ1zhGdhZDsDGGnnm1k5Z+9LLlnfsFye4jigvbN3eA/Ta8hQmBNmEHXoozpmpKK1x8dCePQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/draco3d": "^1.4.0", "@types/draco3d": "^1.4.0",
@ -13121,15 +13172,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.0.1", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.1.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.1.0.tgz",
"integrity": "sha512-V3Y+MdfhawxEjE16dWpb7/IOgeXnLwAEEkS7v8oDqNcR1oYlqWhGH/iHqHdKVdpWme1VPZ0SoywXAkCqawj2eQ==", "integrity": "sha512-prB2U3jXPJLpo1iVLN338Lvolh6OrcCZO+9Yv6AR+tvegPPptYCDBIHiEEUdqRi8gAv2bXNKfMUrgAd2ejn/ow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.0.1", "@typescript-eslint/eslint-plugin": "8.1.0",
"@typescript-eslint/parser": "8.0.1", "@typescript-eslint/parser": "8.1.0",
"@typescript-eslint/utils": "8.0.1" "@typescript-eslint/utils": "8.1.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -16,7 +16,7 @@
"@tanstack/react-table": "^8.20.1", "@tanstack/react-table": "^8.20.1",
"@uiw/codemirror-themes": "^4.23.0", "@uiw/codemirror-themes": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0", "@uiw/react-codemirror": "^4.23.0",
"axios": "^1.7.3", "axios": "^1.7.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.3.24", "framer-motion": "^11.3.24",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
@ -24,7 +24,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-icons": "^5.2.1", "react-icons": "^5.3.0",
"react-intl": "^6.6.8", "react-intl": "^6.6.8",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-pdf": "^9.1.0", "react-pdf": "^9.1.0",
@ -57,7 +57,7 @@
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
"ts-jest": "^29.2.4", "ts-jest": "^29.2.4",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.1", "typescript-eslint": "^8.1.0",
"vite": "^5.4.0" "vite": "^5.4.0"
}, },
"jest": { "jest": {

View File

@ -11,7 +11,7 @@ function ApplicationLayout() {
const { viewportHeight, mainHeight, showScroll } = useConceptOptions(); const { viewportHeight, mainHeight, showScroll } = useConceptOptions();
return ( return (
<NavigationState> <NavigationState>
<div className='min-w-[20rem] clr-app antialiased'> <div className='min-w-[20rem] clr-app antialiased h-full'>
<ConceptToaster <ConceptToaster
className='mt-[4rem] text-sm' // prettier: split lines className='mt-[4rem] text-sm' // prettier: split lines
autoClose={3000} autoClose={3000}
@ -29,7 +29,7 @@ function ApplicationLayout() {
}} }}
> >
<main <main
className='w-full cc-scroll-y' className='w-full h-full cc-scroll-y'
style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }} style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}
> >
<Outlet /> <Outlet />

View File

@ -6,6 +6,7 @@ import { pdfjs } from 'react-pdf';
import { AuthState } from '@/context/AuthContext'; import { AuthState } from '@/context/AuthContext';
import { OptionsState } from '@/context/ConceptOptionsContext'; import { OptionsState } from '@/context/ConceptOptionsContext';
import { GlobalOssState } from '@/context/GlobalOssContext';
import { LibraryState } from '@/context/LibraryContext'; import { LibraryState } from '@/context/LibraryContext';
import { UsersState } from '@/context/UsersContext'; import { UsersState } from '@/context/UsersContext';
@ -37,9 +38,11 @@ function GlobalProviders({ children }: { children: React.ReactNode }) {
<UsersState> <UsersState>
<AuthState> <AuthState>
<LibraryState> <LibraryState>
<GlobalOssState>
{children} {children}
</GlobalOssState>
</LibraryState> </LibraryState>
</AuthState> </AuthState>
</UsersState> </UsersState>

View File

@ -6,6 +6,7 @@ import {
IInputCreatedResponse, IInputCreatedResponse,
IOperationCreateData, IOperationCreateData,
IOperationCreatedResponse, IOperationCreatedResponse,
IOperationDeleteData,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData, IOperationSetInputData,
IOperationUpdateData, IOperationUpdateData,
@ -40,7 +41,7 @@ export function postCreateOperation(
}); });
} }
export function patchDeleteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) { export function patchDeleteOperation(oss: string, request: FrontExchange<IOperationDeleteData, IOperationSchemaData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/oss/${oss}/delete-operation`, endpoint: `/api/oss/${oss}/delete-operation`,
request: request request: request

View File

@ -5,7 +5,7 @@ import { useMemo } from 'react';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { OssNodeInternal } from '@/models/miscellaneous'; import { OssNodeInternal } from '@/models/miscellaneous';
import { ICstSubstituteEx } from '@/models/oss'; import { ICstSubstituteEx, OperationType } from '@/models/oss';
import { labelOperationType } from '@/utils/labels'; import { labelOperationType } from '@/utils/labels';
import { IconPageRight } from '../Icons'; import { IconPageRight } from '../Icons';
@ -62,11 +62,16 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
); );
return ( return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'> <Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense'>
<h2>{node.data.operation.alias}</h2> <h2>{node.data.operation.alias}</h2>
<p> <p>
<b>Тип:</b> {labelOperationType(node.data.operation.operation_type)} <b>Тип:</b> {labelOperationType(node.data.operation.operation_type)}
</p> </p>
{!node.data.operation.is_owned ? (
<p>
<b>КС не принадлежит ОСС</b>
</p>
) : null}
{node.data.operation.title ? ( {node.data.operation.title ? (
<p> <p>
<b>Название: </b> <b>Название: </b>
@ -79,10 +84,13 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
{node.data.operation.comment} {node.data.operation.comment}
</p> </p>
) : null} ) : null}
<p> {node.data.operation.substitutions.length > 0 ? (
<b>Положение:</b> [{node.xPos}, {node.yPos}] table
</p> ) : node.data.operation.operation_type !== OperationType.INPUT ? (
{node.data.operation.substitutions.length > 0 ? table : null} <p>
<b>Отождествления:</b> Отсутствуют
</p>
) : null}
</Tooltip> </Tooltip>
); );
} }

View File

@ -45,15 +45,15 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
id: 'alias', id: 'alias',
header: 'Шифр', header: 'Шифр',
size: 150, size: 300,
minSize: 80, minSize: 150,
maxSize: 150 maxSize: 300
}), }),
columnHelper.accessor('title', { columnHelper.accessor('title', {
id: 'title', id: 'title',
header: 'Название', header: 'Название',
size: 1200, size: 1200,
minSize: 200, minSize: 300,
maxSize: 1200, maxSize: 1200,
cell: props => <div className='text-ellipsis'>{props.getValue()}</div> cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
}), }),

View File

@ -84,10 +84,10 @@ function PickSubstitutions({
const substitutionData: IMultiSubstitution[] = useMemo( const substitutionData: IMultiSubstitution[] = useMemo(
() => () =>
substitutions.map(item => ({ substitutions.map(item => ({
original_source: getSchemaByCst(item.original), original_source: getSchemaByCst(item.original)!,
original: getConstituenta(item.original), original: getConstituenta(item.original)!,
substitution: getConstituenta(item.substitution), substitution: getConstituenta(item.substitution)!,
substitution_source: getSchemaByCst(item.substitution) substitution_source: getSchemaByCst(item.substitution)!
})), })),
[getConstituenta, getSchemaByCst, substitutions] [getConstituenta, getSchemaByCst, substitutions]
); );
@ -138,37 +138,31 @@ function PickSubstitutions({
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', { columnHelper.accessor(item => item.substitution_source.alias, {
id: 'left_schema', id: 'left_schema',
size: 100, size: 100,
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div> cell: props => <div className='min-w-[10.5rem] text-ellipsis text-left'>{props.getValue()}</div>
}), }),
columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', { columnHelper.accessor(item => item.substitution.alias, {
id: 'left_alias', id: 'left_alias',
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.substitution ? ( <BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} />
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.display({ columnHelper.display({
id: 'status', id: 'status',
size: 40, size: 40,
cell: () => <IconPageRight size='1.2rem' /> cell: () => <IconPageRight size='1.2rem' />
}), }),
columnHelper.accessor(item => item.original?.alias ?? 'N/A', { columnHelper.accessor(item => item.original.alias, {
id: 'right_alias', id: 'right_alias',
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.original ? ( <BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} />
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', { columnHelper.accessor(item => item.original_source.alias, {
id: 'right_schema', id: 'right_schema',
size: 100, size: 100,
cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div> cell: props => <div className='min-w-[8rem] text-ellipsis text-right'>{props.getValue()}</div>

View File

@ -27,7 +27,7 @@ function Checkbox({
}: CheckboxProps) { }: CheckboxProps) {
const cursor = useMemo(() => { const cursor = useMemo(() => {
if (disabled) { if (disabled) {
return 'cursor-auto'; return 'cursor-arrow';
} else if (setValue) { } else if (setValue) {
return 'cursor-pointer'; return 'cursor-pointer';
} else { } else {

View File

@ -25,7 +25,7 @@ function CheckboxTristate({
}: CheckboxTristateProps) { }: CheckboxTristateProps) {
const cursor = useMemo(() => { const cursor = useMemo(() => {
if (disabled) { if (disabled) {
return 'cursor-auto'; return 'cursor-arrow';
} else if (setValue) { } else if (setValue) {
return 'cursor-pointer'; return 'cursor-pointer';
} else { } else {

View File

@ -0,0 +1,111 @@
'use client';
import { createContext, useCallback, useContext, useState } from 'react';
import { ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { LibraryItemID } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { contextOutsideScope } from '@/utils/labels';
import { useLibrary } from './LibraryContext';
interface IGlobalOssContext {
schema: IOperationSchema | undefined;
setID: (id: string | undefined) => void;
setData: (data: IOperationSchemaData) => void;
loading: boolean;
loadingError: ErrorData;
isValid: boolean;
invalidate: () => void;
invalidateItem: (target: LibraryItemID) => void;
reload: (callback?: () => void) => void;
}
const GlobalOssContext = createContext<IGlobalOssContext | null>(null);
export const useGlobalOss = (): IGlobalOssContext => {
const context = useContext(GlobalOssContext);
if (context === null) {
throw new Error(contextOutsideScope('useGlobalOss', 'GlobalOssState'));
}
return context;
};
interface GlobalOssStateProps {
children: React.ReactNode;
}
export const GlobalOssState = ({ children }: GlobalOssStateProps) => {
const library = useLibrary();
const [isValid, setIsValid] = useState(false);
const [ossID, setIdInternal] = useState<string | undefined>(undefined);
const {
schema: schema, // prettier: split lines
error: loadingError,
setSchema: setDataInternal,
loading: loading,
reload: reloadInternal
} = useOssDetails({ target: ossID, items: library.items });
const reload = useCallback(
(callback?: () => void) => {
reloadInternal(undefined, () => {
setIsValid(true);
if (callback) callback();
});
},
[reloadInternal]
);
const setData = useCallback(
(data: IOperationSchemaData) => {
setDataInternal(data);
setIsValid(true);
},
[setDataInternal]
);
const setID = useCallback(
(id: string | undefined) => {
setIdInternal(prev => {
if (prev === id && !isValid) {
reload();
}
return id;
});
},
[setIdInternal, isValid, reload]
);
const invalidate = useCallback(() => {
setIsValid(false);
}, []);
const invalidateItem = useCallback(
(target: LibraryItemID) => {
if (schema?.schemas.includes(target)) {
setIsValid(false);
}
},
[schema]
);
return (
<GlobalOssContext.Provider
value={{
schema,
setID,
setData,
loading,
loadingError,
reload,
isValid,
invalidateItem,
invalidate
}}
>
{children}
</GlobalOssContext.Provider>
);
};

View File

@ -13,13 +13,11 @@ import {
} from '@/backend/library'; } from '@/backend/library';
import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms'; import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { FolderTree } from '@/models/FolderTree'; import { FolderTree } from '@/models/FolderTree';
import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library'; import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'; import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader'; import { RSFormLoader } from '@/models/RSFormLoader';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -36,17 +34,10 @@ interface ILibraryContext {
loadingError: ErrorData; loadingError: ErrorData;
setLoadingError: (error: ErrorData) => void; setLoadingError: (error: ErrorData) => void;
globalOSS: IOperationSchema | undefined;
setGlobalID: (id: string | undefined) => void;
setGlobalOSS: (data: IOperationSchemaData) => void;
ossLoading: boolean;
ossError: ErrorData;
processing: boolean; processing: boolean;
processingError: ErrorData; processingError: ErrorData;
setProcessingError: (error: ErrorData) => void; setProcessingError: (error: ErrorData) => void;
reloadOSS: (callback?: () => void) => void;
reloadItems: (callback?: () => void) => void; reloadItems: (callback?: () => void) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[]; applyFilter: (params: ILibraryFilter) => ILibraryItem[];
@ -84,22 +75,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]); const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
const [ossID, setGlobalID] = useState<string | undefined>(undefined);
const {
schema: globalOSS, // prettier: split lines
error: ossError,
setSchema: setGlobalOSS,
loading: ossLoading,
reload: reloadOssInternal
} = useOssDetails({ target: ossID });
const reloadOSS = useCallback(
(callback?: () => void) => {
reloadOssInternal(setProcessing, callback);
},
[reloadOssInternal]
);
const folders = useMemo(() => { const folders = useMemo(() => {
const result = new FolderTree(); const result = new FolderTree();
result.addPath(LocationHead.USER, 0); result.addPath(LocationHead.USER, 0);
@ -280,17 +255,11 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
1 1
); );
} }
if (globalOSS?.schemas.includes(target)) { if (callback) callback();
reloadOSS(() => {
if (callback) callback();
});
} else {
if (callback) callback();
}
}) })
}); });
}, },
[reloadItems, reloadOSS, user, globalOSS] [reloadItems, user]
); );
const cloneItem = useCallback( const cloneItem = useCallback(
@ -326,20 +295,12 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
loading, loading,
loadingError, loadingError,
setLoadingError, setLoadingError,
reloadItems,
processing, processing,
processingError, processingError,
setProcessingError, setProcessingError,
globalOSS,
setGlobalID,
setGlobalOSS,
ossLoading,
ossError,
reloadOSS,
reloadItems,
applyFilter, applyFilter,
createItem, createItem,
cloneItem, cloneItem,

View File

@ -27,6 +27,7 @@ import { ILibraryUpdateData } from '@/models/library';
import { import {
IOperationCreateData, IOperationCreateData,
IOperationData, IOperationData,
IOperationDeleteData,
IOperationSchema, IOperationSchema,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData, IOperationSetInputData,
@ -38,6 +39,7 @@ import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext'; import { useLibrary } from './LibraryContext';
interface IOssContext { interface IOssContext {
@ -45,7 +47,7 @@ interface IOssContext {
itemID: string; itemID: string;
loading: boolean; loading: boolean;
errorLoading: ErrorData; loadingError: ErrorData;
processing: boolean; processing: boolean;
processingError: ErrorData; processingError: ErrorData;
@ -63,7 +65,7 @@ interface IOssContext {
savePositions: (data: IPositionsData, callback?: () => void) => void; savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperationData>) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperationData>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: IOperationDeleteData, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void; createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
setInput: (data: IOperationSetInputData, callback?: () => void) => void; setInput: (data: IOperationSetInputData, callback?: () => void) => void;
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void; updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
@ -86,7 +88,8 @@ interface OssStateProps {
export const OssState = ({ itemID, children }: OssStateProps) => { export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary(); const library = useLibrary();
const schema = library.globalOSS; const oss = useGlobalOss();
const model = oss.schema;
const { user } = useAuth(); const { user } = useAuth();
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
@ -94,25 +97,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
const [toggleTracking, setToggleTracking] = useState(false); const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo(() => { const isOwned = useMemo(() => {
return user?.id === schema?.owner || false; return user?.id === model?.owner || false;
}, [user, schema?.owner]); }, [user, model?.owner]);
const isSubscribed = useMemo(() => { const isSubscribed = useMemo(() => {
if (!user || !schema || !user.id) { if (!user || !model || !user.id) {
return false; return false;
} }
return schema.subscribers.includes(user.id); return model.subscribers.includes(user.id);
}, [user, schema, toggleTracking]); }, [user, model, toggleTracking]);
useEffect(() => { useEffect(() => {
if (schema?.id !== Number(itemID)) { oss.setID(itemID);
library.setGlobalID(itemID); }, [itemID, oss.setID]);
}
}, [itemID, schema, library]);
const update = useCallback( const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => { (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -122,19 +123,19 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
const fullData: IOperationSchemaData = Object.assign(schema, newData); const fullData: IOperationSchemaData = Object.assign(model, newData);
library.setGlobalOSS(fullData); oss.setData(fullData);
library.localUpdateItem(newData); library.localUpdateItem(newData);
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.localUpdateItem, oss.setData]
); );
const subscribe = useCallback( const subscribe = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
if (!schema || !user) { if (!model || !user) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -143,23 +144,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
if (user.id && !schema.subscribers.includes(user.id)) { if (user.id && !model.subscribers.includes(user.id)) {
schema.subscribers.push(user.id); model.subscribers.push(user.id);
} }
if (!user.subscriptions.includes(schema.id)) { if (!user.subscriptions.includes(model.id)) {
user.subscriptions.push(schema.id); user.subscriptions.push(model.id);
} }
setToggleTracking(prev => !prev); setToggleTracking(prev => !prev);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, user, schema] [itemID, user, model]
); );
const unsubscribe = useCallback( const unsubscribe = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
if (!schema || !user) { if (!model || !user) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -168,23 +169,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
if (user.id && schema.subscribers.includes(user.id)) { if (user.id && model.subscribers.includes(user.id)) {
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1); model.subscribers.splice(model.subscribers.indexOf(user.id), 1);
} }
if (user.subscriptions.includes(schema.id)) { if (user.subscriptions.includes(model.id)) {
user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1); user.subscriptions.splice(user.subscriptions.indexOf(model.id), 1);
} }
setToggleTracking(prev => !prev); setToggleTracking(prev => !prev);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema, user] [itemID, model, user]
); );
const setOwner = useCallback( const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => { (newOwner: UserID, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -196,18 +197,18 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
schema.owner = newOwner; model.owner = newOwner;
library.localUpdateItem(schema); library.localUpdateItem(model);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.localUpdateItem]
); );
const setAccessPolicy = useCallback( const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy, callback?: () => void) => { (newPolicy: AccessPolicy, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -219,18 +220,18 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
schema.access_policy = newPolicy; model.access_policy = newPolicy;
library.localUpdateItem(schema); library.localUpdateItem(model);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.localUpdateItem]
); );
const setLocation = useCallback( const setLocation = useCallback(
(newLocation: string, callback?: () => void) => { (newLocation: string, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -242,18 +243,18 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
schema.location = newLocation; model.location = newLocation;
library.localUpdateItem(schema); library.localUpdateItem(model);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.localUpdateItem]
); );
const setEditors = useCallback( const setEditors = useCallback(
(newEditors: UserID[], callback?: () => void) => { (newEditors: UserID[], callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -265,12 +266,12 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: () => { onSuccess: () => {
schema.editors = newEditors; model.editors = newEditors;
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema] [itemID, model]
); );
const savePositions = useCallback( const savePositions = useCallback(
@ -287,7 +288,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
} }
}); });
}, },
[itemID, library] [itemID, library.localUpdateTimestamp]
); );
const createOperation = useCallback( const createOperation = useCallback(
@ -299,17 +300,17 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData.oss); oss.setData(newData.oss);
library.localUpdateTimestamp(newData.oss.id); library.localUpdateTimestamp(newData.oss.id);
if (callback) callback(newData.new_operation); if (callback) callback(newData.new_operation);
} }
}); });
}, },
[itemID, library] [itemID, library.localUpdateTimestamp, oss.setData]
); );
const deleteOperation = useCallback( const deleteOperation = useCallback(
(data: ITargetOperation, callback?: () => void) => { (data: IOperationDeleteData, callback?: () => void) => {
setProcessingError(undefined); setProcessingError(undefined);
patchDeleteOperation(itemID, { patchDeleteOperation(itemID, {
data: data, data: data,
@ -317,13 +318,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData); oss.setData(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, library] [itemID, library.localUpdateTimestamp, oss.setData]
); );
const createInput = useCallback( const createInput = useCallback(
@ -335,19 +336,19 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData.oss); oss.setData(newData.oss);
library.reloadItems(() => { library.reloadItems(() => {
if (callback) callback(newData.new_schema); if (callback) callback(newData.new_schema);
}); });
} }
}); });
}, },
[itemID, library] [itemID, library.reloadItems, oss.setData]
); );
const setInput = useCallback( const setInput = useCallback(
(data: IOperationSetInputData, callback?: () => void) => { (data: IOperationSetInputData, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -357,18 +358,18 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData); oss.setData(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.localUpdateTimestamp, oss.setData]
); );
const updateOperation = useCallback( const updateOperation = useCallback(
(data: IOperationUpdateData, callback?: () => void) => { (data: IOperationUpdateData, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -378,19 +379,19 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
}); });
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.reloadItems, oss.setData]
); );
const executeOperation = useCallback( const executeOperation = useCallback(
(data: ITargetOperation, callback?: () => void) => { (data: ITargetOperation, callback?: () => void) => {
if (!schema) { if (!model) {
return; return;
} }
setProcessingError(undefined); setProcessingError(undefined);
@ -400,23 +401,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
library.setGlobalOSS(newData); oss.setData(newData);
library.reloadItems(() => { library.reloadItems(() => {
if (callback) callback(); if (callback) callback();
}); });
} }
}); });
}, },
[itemID, schema, library] [itemID, model, library.reloadItems, oss.setData]
); );
return ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
schema, schema: model,
itemID, itemID,
loading: library.ossLoading, loading: oss.loading,
errorLoading: library.ossError, loadingError: oss.loadingError,
processing, processing,
processingError, processingError,
isOwned, isOwned,

View File

@ -53,6 +53,7 @@ import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext'; import { useLibrary } from './LibraryContext';
interface IRSFormContext { interface IRSFormContext {
@ -116,6 +117,7 @@ interface RSFormStateProps {
export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) => { export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) => {
const library = useLibrary(); const library = useLibrary();
const oss = useGlobalOss();
const { user } = useAuth(); const { user } = useAuth();
const { const {
schema, // prettier: split lines schema, // prettier: split lines
@ -159,15 +161,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(Object.assign(schema, newData)); setSchema(Object.assign(schema, newData));
library.localUpdateItem(newData); library.localUpdateItem(newData);
if (library.globalOSS?.schemas.includes(newData.id)) { oss.invalidateItem(newData.id);
library.reloadOSS(() => { if (callback) callback(newData);
if (callback) callback(newData);
});
} else if (callback) callback(newData);
} }
}); });
}, },
[itemID, setSchema, schema, library] [itemID, setSchema, schema, library.localUpdateItem, oss.invalidateItem]
); );
const upload = useCallback( const upload = useCallback(
@ -188,7 +187,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, setSchema, schema, library] [itemID, setSchema, schema, library.localUpdateItem]
); );
const subscribe = useCallback( const subscribe = useCallback(
@ -261,7 +260,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, schema, library] [itemID, schema, library.localUpdateItem]
); );
const setAccessPolicy = useCallback( const setAccessPolicy = useCallback(
@ -284,7 +283,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, schema, library] [itemID, schema, library.localUpdateItem]
); );
const setLocation = useCallback( const setLocation = useCallback(
@ -306,7 +305,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, schema, library] [itemID, schema, library.reloadItems]
); );
const setEditors = useCallback( const setEditors = useCallback(
@ -344,13 +343,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
if (callback) callback(); if (callback) callback();
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
[itemID, schema, library, user, setSchema] [itemID, schema, user, setSchema, library.localUpdateTimestamp, oss.invalidateItem]
); );
const restoreOrder = useCallback( const restoreOrder = useCallback(
@ -370,7 +368,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, schema, library, user, setSchema] [itemID, schema, user, setSchema, library.localUpdateTimestamp]
); );
const produceStructure = useCallback( const produceStructure = useCallback(
@ -384,11 +382,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id);
if (callback) callback(newData.cst_list); if (callback) callback(newData.cst_list);
} }
}); });
}, },
[setSchema, library, itemID] [setSchema, itemID, library.localUpdateTimestamp, oss.invalidateItem]
); );
const download = useCallback( const download = useCallback(
@ -415,13 +414,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id);
if (callback) callback(newData.new_cst); if (callback) callback(newData.new_cst);
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
[itemID, library, setSchema] [itemID, setSchema, library.localUpdateTimestamp, oss.invalidateItem]
); );
const cstDelete = useCallback( const cstDelete = useCallback(
@ -435,13 +433,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
if (callback) callback(); if (callback) callback();
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
[itemID, library, setSchema] [itemID, setSchema, library.localUpdateTimestamp, oss.invalidateItem]
); );
const cstUpdate = useCallback( const cstUpdate = useCallback(
@ -455,13 +452,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => onSuccess: newData =>
reload(setProcessing, () => { reload(setProcessing, () => {
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(Number(itemID));
oss.invalidateItem(Number(itemID));
if (callback) callback(newData); if (callback) callback(newData);
// TODO: deal with OSS cache invalidation
}) })
}); });
}, },
[itemID, library, reload] [itemID, reload, library.localUpdateTimestamp, oss.invalidateItem]
); );
const cstRename = useCallback( const cstRename = useCallback(
@ -475,15 +471,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.schema); setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id); library.localUpdateTimestamp(newData.schema.id);
if (library.globalOSS?.schemas.includes(newData.schema.id)) { oss.invalidateItem(newData.schema.id);
library.reloadOSS(() => { if (callback) callback(newData.new_cst);
if (callback) callback(newData.new_cst);
});
} else if (callback) callback(newData.new_cst);
} }
}); });
}, },
[setSchema, library, itemID] [setSchema, itemID, library.localUpdateTimestamp, oss.invalidateItem]
); );
const cstSubstitute = useCallback( const cstSubstitute = useCallback(
@ -497,15 +490,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (library.globalOSS?.schemas.includes(newData.id)) { oss.invalidateItem(newData.id);
library.reloadOSS(() => { if (callback) callback();
if (callback) callback();
});
} else if (callback) callback();
} }
}); });
}, },
[setSchema, library, itemID] [setSchema, itemID, library.localUpdateTimestamp, oss.invalidateItem]
); );
const cstMoveTo = useCallback( const cstMoveTo = useCallback(
@ -523,7 +513,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, library, setSchema] [itemID, setSchema, library.localUpdateTimestamp]
); );
const versionCreate = useCallback( const versionCreate = useCallback(
@ -541,7 +531,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[itemID, library, setSchema] [itemID, setSchema, library.localUpdateTimestamp]
); );
const findPredecessor = useCallback((data: ITargetCst, callback: (reference: IConstituentaReference) => void) => { const findPredecessor = useCallback((data: ITargetCst, callback: (reference: IConstituentaReference) => void) => {
@ -612,7 +602,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
} }
}); });
}, },
[setSchema, library] [setSchema, library.localUpdateItem]
); );
const inlineSynthesis = useCallback( const inlineSynthesis = useCallback(
@ -625,14 +615,13 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); setSchema(newData);
library.localUpdateTimestamp(Number(itemID)); library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
if (callback) callback(newData); if (callback) callback(newData);
// TODO: deal with OSS cache invalidation
} }
}); });
}, },
[library, itemID, setSchema] [itemID, setSchema, library.localUpdateTimestamp, oss.invalidateItem]
); );
return ( return (

View File

@ -31,10 +31,6 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
setSelected(newValue); setSelected(newValue);
}, []); }, []);
function handleSubmit() {
onSubmit(selected);
}
return ( return (
<Modal <Modal
overflowVisible overflowVisible
@ -42,7 +38,7 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
submitText='Подтвердить выбор' submitText='Подтвердить выбор'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={() => onSubmit(selected)}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')} className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')}
> >
<div className='flex justify-between gap-3 items-center'> <div className='flex justify-between gap-3 items-center'>

View File

@ -34,10 +34,6 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
setBody(newValue.length > 3 ? newValue.substring(3) : ''); setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []); }, []);
function handleSubmit() {
onChangeLocation(location);
}
return ( return (
<Modal <Modal
overflowVisible overflowVisible
@ -46,7 +42,7 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`} submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={() => onChangeLocation(location)}
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')} className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')}
> >
<div className='flex flex-col gap-2 w-[7rem] h-min'> <div className='flex flex-col gap-2 w-[7rem] h-min'>

View File

@ -0,0 +1,64 @@
'use client';
import clsx from 'clsx';
import { useState } from 'react';
import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal';
import TextInput from '@/components/ui/TextInput';
import { IOperation } from '@/models/oss';
interface DlgDeleteOperationProps extends Pick<ModalProps, 'hideWindow'> {
target: IOperation;
onSubmit: (keepConstituents: boolean, deleteSchema: boolean) => void;
}
function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperationProps) {
const [keepConstituents, setKeepConstituents] = useState(false);
const [deleteSchema, setDeleteSchema] = useState(false);
function handleSubmit() {
onSubmit(keepConstituents, deleteSchema);
}
return (
<Modal
overflowVisible
header='Удаление операции'
submitText='Подтвердить удаление'
hideWindow={hideWindow}
canSubmit={true}
onSubmit={handleSubmit}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column', 'select-none')}
>
<TextInput
disabled
dense
noBorder
id='operation_alias'
label='Операция'
className='w-full'
value={target.alias}
/>
<Checkbox
label='Сохранить наследованные конституенты'
titleHtml='Наследованные конституенты <br/>превратятся в дописанные'
value={keepConstituents}
setValue={setKeepConstituents}
/>
<Checkbox
label='Удалить схему'
titleHtml={
!target.is_owned || target.result === undefined
? 'Привязанную схему нельзя удалить'
: 'Удалить схему вместе с операцией'
}
value={deleteSchema}
setValue={setDeleteSchema}
disabled={!target.is_owned || target.result === undefined}
/>
</Modal>
);
}
export default DlgDeleteOperation;

View File

@ -63,6 +63,25 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
cache.preload(schemasIDs); cache.preload(schemasIDs);
}, [schemasIDs]); }, [schemasIDs]);
useEffect(() => {
if (cache.loading || schemas.length !== schemasIDs.length) {
return;
}
setSubstitutions(prev =>
prev.filter(sub => {
const original = cache.getSchemaByCst(sub.original);
if (!original || !schemasIDs.includes(original.id)) {
return false;
}
const substitution = cache.getSchemaByCst(sub.substitution);
if (!substitution || !schemasIDs.includes(substitution.id)) {
return false;
}
return true;
})
);
}, [schemasIDs, schemas, cache.loading]);
const handleSubmit = () => { const handleSubmit = () => {
const data: IOperationUpdateData = { const data: IOperationUpdateData = {
target: target.id, target: target.id,

View File

@ -2,13 +2,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import PickMultiOperation from '@/components/select/PickMultiOperation';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperationSchema, OperationID } from '@/models/oss'; import { IOperationSchema, OperationID } from '@/models/oss';
import PickMultiOperation from '../../components/select/PickMultiOperation';
interface TabArgumentsProps { interface TabArgumentsProps {
oss: IOperationSchema; oss: IOperationSchema;
target: OperationID; target: OperationID;

View File

@ -15,17 +15,12 @@ interface DlgGraphParamsProps extends Pick<ModalProps, 'hideWindow'> {
function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) { function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) {
const [params, updateParams] = usePartialUpdate(initial); const [params, updateParams] = usePartialUpdate(initial);
function handleSubmit() {
hideWindow();
onConfirm(params);
}
return ( return (
<Modal <Modal
canSubmit canSubmit
hideWindow={hideWindow} hideWindow={hideWindow}
header='Настройки графа термов' header='Настройки графа термов'
onSubmit={handleSubmit} onSubmit={() => onConfirm(params)}
submitText='Применить' submitText='Применить'
className='flex gap-6 justify-between px-6 pb-3 w-[30rem]' className='flex gap-6 justify-between px-6 pb-3 w-[30rem]'
> >

View File

@ -26,8 +26,6 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [cstData, updateData] = usePartialUpdate(initial); const [cstData, updateData] = usePartialUpdate(initial);
const handleSubmit = () => onRename(cstData);
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema && initial && cstData.cst_type !== initial.cst_type) { if (schema && initial && cstData.cst_type !== initial.cst_type) {
updateData({ alias: generateAlias(cstData.cst_type, schema) }); updateData({ alias: generateAlias(cstData.cst_type, schema) });
@ -47,7 +45,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'} submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={() => onRename(cstData)}
className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex justify-center items-center')} className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex justify-center items-center')}
> >
<SelectSingle <SelectSingle

View File

@ -5,10 +5,11 @@ import { useCallback, useEffect, useState } from 'react';
import { getOssDetails } from '@/backend/oss'; import { getOssDetails } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { ILibraryItem } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader'; import { OssLoader } from '@/models/OssLoader';
function useOssDetails({ target }: { target?: string }) { function useOssDetails({ target, items }: { target?: string; items: ILibraryItem[] }) {
const { loading: userLoading } = useAuth(); const { loading: userLoading } = useAuth();
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined); const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
const [loading, setLoading] = useState(target != undefined); const [loading, setLoading] = useState(target != undefined);
@ -19,7 +20,7 @@ function useOssDetails({ target }: { target?: string }) {
setInner(undefined); setInner(undefined);
return; return;
} }
const newSchema = new OssLoader(data).produceOSS(); const newSchema = new OssLoader(data, items).produceOSS();
setInner(newSchema); setInner(newSchema);
} }

View File

@ -55,11 +55,11 @@ function useRSFormCache() {
useEffect(() => { useEffect(() => {
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id)); const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
setPending([]);
if (ids.length === 0) { if (ids.length === 0) {
return; return;
} }
setProcessing(prev => [...prev, ...ids]); setProcessing(prev => [...prev, ...ids]);
setPending([]);
ids.forEach(id => ids.forEach(id =>
getRSFormDetails(String(id), '', { getRSFormDetails(String(id), '', {
showError: false, showError: false,

View File

@ -3,7 +3,7 @@
*/ */
import { Graph } from './Graph'; import { Graph } from './Graph';
import { LibraryItemID } from './library'; import { ILibraryItem, LibraryItemID } from './library';
import { import {
IOperation, IOperation,
IOperationSchema, IOperationSchema,
@ -21,10 +21,12 @@ export class OssLoader {
private oss: IOperationSchemaData; private oss: IOperationSchemaData;
private graph: Graph = new Graph(); private graph: Graph = new Graph();
private operationByID = new Map<OperationID, IOperation>(); private operationByID = new Map<OperationID, IOperation>();
private schemas: LibraryItemID[] = []; private schemaIDs: LibraryItemID[] = [];
private items: ILibraryItem[];
constructor(input: IOperationSchemaData) { constructor(input: IOperationSchemaData, items: ILibraryItem[]) {
this.oss = input; this.oss = input;
this.items = items;
} }
produceOSS(): IOperationSchema { produceOSS(): IOperationSchema {
@ -36,7 +38,7 @@ export class OssLoader {
result.operationByID = this.operationByID; result.operationByID = this.operationByID;
result.graph = this.graph; result.graph = this.graph;
result.schemas = this.schemas; result.schemas = this.schemaIDs;
result.stats = this.calculateStats(); result.stats = this.calculateStats();
return result; return result;
} }
@ -53,12 +55,14 @@ export class OssLoader {
} }
private extractSchemas() { private extractSchemas() {
this.schemas = this.oss.items.map(operation => operation.result).filter(item => item !== null); this.schemaIDs = this.oss.items.map(operation => operation.result).filter(item => item !== null);
} }
private inferOperationAttributes() { private inferOperationAttributes() {
this.graph.topologicalOrder().forEach(operationID => { this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!; const operation = this.operationByID.get(operationID)!;
const schema = this.items.find(item => item.id === operation.result);
operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
operation.arguments = this.oss.arguments operation.arguments = this.oss.arguments
.filter(item => item.operation === operationID) .filter(item => item.operation === operationID)
@ -72,7 +76,7 @@ export class OssLoader {
count_operations: items.length, count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length, count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemas.length count_schemas: this.schemaIDs.length
}; };
} }
} }

View File

@ -36,6 +36,7 @@ export interface IOperation {
result: LibraryItemID | null; result: LibraryItemID | null;
is_owned: boolean;
substitutions: ICstSubstituteEx[]; substitutions: ICstSubstituteEx[];
arguments: OperationID[]; arguments: OperationID[];
} }
@ -85,6 +86,14 @@ export interface IOperationUpdateData extends ITargetOperation {
substitutions: ICstSubstitute[] | undefined; substitutions: ICstSubstitute[] | undefined;
} }
/**
* Represents {@link IOperation} data, used in destruction process.
*/
export interface IOperationDeleteData extends ITargetOperation {
keep_constituents: boolean;
delete_schema: boolean;
}
/** /**
* Represents {@link IOperation} data, used in setInput process. * Represents {@link IOperation} data, used in setInput process.
*/ */
@ -119,10 +128,10 @@ export interface ICstSubstituteData {
* Represents substitution for multi synthesis table. * Represents substitution for multi synthesis table.
*/ */
export interface IMultiSubstitution { export interface IMultiSubstitution {
original_source: ILibraryItem | undefined; original_source: ILibraryItem;
original: IConstituenta | undefined; original: IConstituenta;
substitution: IConstituenta | undefined; substitution: IConstituenta;
substitution_source: ILibraryItem | undefined; substitution_source: ILibraryItem;
} }
/** /**

View File

@ -33,6 +33,13 @@ function InputNode(node: OssNodeInternal) {
disabled={!hasFile} disabled={!hasFile}
/> />
</Overlay> </Overlay>
{!node.data.operation.is_owned ? (
<Overlay position='left-[0.2rem] top-[0.1rem]'>
<div className='border rounded-none clr-input h-[1.3rem]'></div>
</Overlay>
) : null}
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'>
{node.data.label} {node.data.label}
{controller.showTooltip && !node.dragging ? ( {controller.showTooltip && !node.dragging ? (

View File

@ -155,7 +155,7 @@ function NodeContextMenu({
<DropdownButton <DropdownButton
text='Удалить операцию' text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!controller.isMutable || controller.isProcessing} disabled={!controller.isMutable || controller.isProcessing || !controller.canDelete(operation.id)}
onClick={handleDeleteOperation} onClick={handleDeleteOperation}
/> />
</Dropdown> </Dropdown>

View File

@ -33,6 +33,12 @@ function OperationNode(node: OssNodeInternal) {
/> />
</Overlay> </Overlay>
{!node.data.operation.is_owned ? (
<Overlay position='left-[0.2rem] top-[0.1rem]'>
<div className='border rounded-none clr-input h-[1.3rem]'></div>
</Overlay>
) : null}
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center'>
{node.data.label} {node.data.label}
{controller.showTooltip && !node.dragging ? ( {controller.showTooltip && !node.dragging ? (

View File

@ -132,8 +132,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
const positions = getPositions(); const positions = getPositions();
if (positions.length == 0) { if (positions.length == 0) {
target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
} } else if (inputs.length <= 1) {
if (inputs.length <= 1) {
let inputsNodes = positions.filter(pos => let inputsNodes = positions.filter(pos =>
controller.schema!.items.find( controller.schema!.items.find(
operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id operation => operation.operation_type === OperationType.INPUT && operation.id === pos.id
@ -167,6 +166,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
target.y += PARAMETER.ossMinDistance; target.y += PARAMETER.ossMinDistance;
} }
} while (flagIntersect); } while (flagIntersect);
controller.promptCreateOperation({ controller.promptCreateOperation({
x: target.x, x: target.x,
y: target.y, y: target.y,
@ -182,12 +182,15 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
if (controller.selected.length !== 1) { if (controller.selected.length !== 1) {
return; return;
} }
controller.deleteOperation(controller.selected[0], getPositions()); handleDeleteOperation(controller.selected[0]);
}, [controller, getPositions]); }, [controller, getPositions]);
const handleDeleteOperation = useCallback( const handleDeleteOperation = useCallback(
(target: OperationID) => { (target: OperationID) => {
controller.deleteOperation(target, getPositions()); if (!controller.canDelete(target)) {
return;
}
controller.promptDeleteOperation(target, getPositions());
}, },
[controller, getPositions] [controller, getPositions]
); );

View File

@ -175,7 +175,11 @@ function ToolbarOssGraph({
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')} titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.selected.length !== 1 || controller.isProcessing} disabled={
controller.selected.length !== 1 ||
controller.isProcessing ||
!controller.canDelete(controller.selected[0])
}
onClick={onDelete} onClick={onDelete}
/> />
</div> </div>

View File

@ -13,17 +13,20 @@ import { useOSS } from '@/context/OssContext';
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
import DlgDeleteOperation from '@/dialogs/DlgDeleteOperation';
import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditEditors from '@/dialogs/DlgEditEditors';
import DlgEditOperation from '@/dialogs/DlgEditOperation'; import DlgEditOperation from '@/dialogs/DlgEditOperation';
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library'; import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
import { Position2D } from '@/models/miscellaneous'; import { Position2D } from '@/models/miscellaneous';
import { import {
IOperationCreateData, IOperationCreateData,
IOperationDeleteData,
IOperationPosition, IOperationPosition,
IOperationSchema, IOperationSchema,
IOperationSetInputData, IOperationSetInputData,
IOperationUpdateData, IOperationUpdateData,
OperationID OperationID,
OperationType
} from '@/models/oss'; } from '@/models/oss';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
@ -62,7 +65,8 @@ export interface IOssEditContext extends ILibraryItemEditor {
savePositions: (positions: IOperationPosition[], callback?: () => void) => void; savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
promptCreateOperation: (props: ICreateOperationPrompt) => void; promptCreateOperation: (props: ICreateOperationPrompt) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; canDelete: (target: OperationID) => boolean;
promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
@ -103,6 +107,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const [showEditLocation, setShowEditLocation] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false);
const [showEditInput, setShowEditInput] = useState(false); const [showEditInput, setShowEditInput] = useState(false);
const [showEditOperation, setShowEditOperation] = useState(false); const [showEditOperation, setShowEditOperation] = useState(false);
const [showDeleteOperation, setShowDeleteOperation] = useState(false);
const [showCreateOperation, setShowCreateOperation] = useState(false); const [showCreateOperation, setShowCreateOperation] = useState(false);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 }); const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
@ -258,15 +263,48 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model, positions] [model, positions]
); );
const deleteOperation = useCallback( const canDelete = useCallback(
(target: OperationID, positions: IOperationPosition[]) => { (target: OperationID) => {
model.deleteOperation({ target: target, positions: positions }, () => if (!model.schema) {
toast.success(information.operationDestroyed) return false;
); }
const operation = model.schema.operationByID.get(target);
if (!operation) {
return false;
}
if (operation.operation_type === OperationType.INPUT) {
return true;
}
return model.schema.graph.expandOutputs([target]).length === 0;
}, },
[model] [model]
); );
const promptDeleteOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
setPositions(positions);
setTargetOperationID(target);
setShowDeleteOperation(true);
},
[model]
);
const deleteOperation = useCallback(
(keepConstituents: boolean, deleteSchema: boolean) => {
if (!targetOperationID) {
return;
}
const data: IOperationDeleteData = {
target: targetOperationID,
positions: positions,
keep_constituents: keepConstituents,
delete_schema: deleteSchema
};
model.deleteOperation(data, () => toast.success(information.operationDestroyed));
},
[model, targetOperationID, positions]
);
const createInput = useCallback( const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => { (target: OperationID, positions: IOperationPosition[]) => {
model.createInput({ target: target, positions: positions }, new_schema => { model.createInput({ target: target, positions: positions }, new_schema => {
@ -334,7 +372,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
openOperationSchema, openOperationSchema,
savePositions, savePositions,
promptCreateOperation, promptCreateOperation,
deleteOperation, canDelete,
promptDeleteOperation,
createInput, createInput,
promptEditInput, promptEditInput,
promptEditOperation, promptEditOperation,
@ -381,6 +420,13 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
onSubmit={handleEditOperation} onSubmit={handleEditOperation}
/> />
) : null} ) : null}
{showDeleteOperation ? (
<DlgDeleteOperation
hideWindow={() => setShowDeleteOperation(false)}
target={targetOperation!}
onSubmit={deleteOperation}
/>
) : null}
</AnimatePresence> </AnimatePresence>
) : null} ) : null}

View File

@ -35,8 +35,8 @@ function OssTabs() {
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH; const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
const { calculateHeight } = useConceptOptions(); const { calculateHeight, setNoFooter } = useConceptOptions();
const { schema, loading, errorLoading } = useOSS(); const { schema, loading, loadingError: errorLoading } = useOSS();
const { destroyItem } = useLibrary(); const { destroyItem } = useLibrary();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
@ -53,6 +53,10 @@ function OssTabs() {
} }
}, [schema, schema?.title]); }, [schema, schema?.title]);
useLayoutEffect(() => {
setNoFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, setNoFooter]);
const navigateTab = useCallback( const navigateTab = useCallback(
(tab: OssTabID) => { (tab: OssTabID) => {
if (!schema) { if (!schema) {
@ -96,7 +100,7 @@ function OssTabs() {
}); });
}, [schema, destroyItem, router]); }, [schema, destroyItem, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const panelHeight = useMemo(() => calculateHeight('1.625rem + 2px'), [calculateHeight]);
const cardPanel = useMemo( const cardPanel = useMemo(
() => ( () => (

View File

@ -14,6 +14,7 @@ import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useGlobalOss } from '@/context/GlobalOssContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
@ -47,6 +48,7 @@ function RSTabs() {
const { setNoFooter, calculateHeight } = useConceptOptions(); const { setNoFooter, calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm(); const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const library = useLibrary(); const library = useLibrary();
const oss = useGlobalOss();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified); useBlockNavigation(isModified);
@ -177,18 +179,19 @@ function RSTabs() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) { if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return; return;
} }
const backToOSS = library.globalOSS?.schemas.includes(schema.id); const backToOSS = oss.schema?.schemas.includes(schema.id);
library.destroyItem(schema.id, () => { library.destroyItem(schema.id, () => {
toast.success(information.itemDestroyed); toast.success(information.itemDestroyed);
if (backToOSS) { if (backToOSS) {
router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH)); oss.invalidate();
router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH));
} else { } else {
router.push(urls.library); router.push(urls.library);
} }
}); });
}, [schema, library, router]); }, [schema, library, oss, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const panelHeight = useMemo(() => calculateHeight('1.625rem + 2px'), [calculateHeight]);
const cardPanel = useMemo( const cardPanel = useMemo(
() => ( () => (