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]:
''' Get all Versions of this item. '''
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')
def test_destroy(self):
response = self.execute(item=self.owned.pk)
self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
self.executeNoContent(item=self.owned.pk)
self.executeForbidden(item=self.unowned.pk)
self.toggle_admin(True)
response = self.execute(item=self.unowned.pk)
self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
self.executeNoContent(item=self.unowned.pk)
@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.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.serializers import RSFormParseSerializer
from apps.users.models import User
@ -67,6 +67,10 @@ class LibraryViewSet(viewsets.ModelViewSet):
if update_list:
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):
if self.action in ['update', 'partial_update']:
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. '''
from typing import Optional
from typing import Optional, cast
from cctext import extract_entities
from django.db.models import QuerySet
from rest_framework.serializers import ValidationError
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 .Inheritance import Inheritance
from .Operation import Operation
from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class OperationSchema:
''' Operations schema API. '''
def __init__(self, model: LibraryItem):
self.model = model
self.cache = OssCache(self)
@staticmethod
def create(**kwargs) -> 'OperationSchema':
@ -75,34 +91,45 @@ class OperationSchema:
def create_operation(self, **kwargs) -> Operation:
''' Insert new operation. '''
result = Operation.objects.create(oss=self.model, **kwargs)
self.save()
result.refresh_from_db()
self.cache.insert_operation(result)
self.save(update_fields=['time_update'])
return result
def delete_operation(self, operation: Operation):
def delete_operation(self, target: Operation, keep_constituents: bool = False):
''' 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
# TODO: trigger on_change effects
self.save()
def set_input(self, target: Operation, schema: Optional[LibraryItem]) -> None:
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
''' 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
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:
target.result = schema
target.alias = schema.alias
target.title = schema.title
target.comment = schema.comment
target.save()
operation.result = schema
operation.alias = schema.alias
operation.title = schema.title
operation.comment = schema.comment
operation.save(update_fields=['result', 'alias', 'title', 'comment'])
# TODO: trigger on_change effects
self.save()
if schema is not None and has_children:
rsform = RSForm(schema)
self.after_create_cst(list(rsform.constituents()), rsform)
self.save(update_fields=['time_update'])
def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None:
''' Set arguments to operation. '''
@ -122,7 +149,7 @@ class OperationSchema:
if not changed:
return
# TODO: trigger on_change effects
self.save()
self.save(update_fields=['time_update'])
def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None:
''' Clear all arguments for operation. '''
@ -153,7 +180,7 @@ class OperationSchema:
return
# TODO: trigger on_change effects
self.save()
self.save(update_fields=['time_update'])
def create_input(self, operation: Operation) -> RSForm:
''' Create input RSForm. '''
@ -169,7 +196,7 @@ class OperationSchema:
Editor.set(schema.model.pk, self.model.editors().values_list('pk', flat=True))
operation.result = schema.model
operation.save()
self.save()
self.save(update_fields=['time_update'])
return schema
def execute_operation(self, operation: Operation) -> bool:
@ -210,5 +237,504 @@ class OperationSchema:
receiver.restore_order()
receiver.reset_aliases()
self.save()
self.save(update_fields=['time_update'])
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. '''
from apps.library.models import LibraryItem
from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import Constituenta, RSForm
from .ChangeManager import ChangeManager
from .OperationSchema import CstSubstitution, OperationSchema
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
@ -13,37 +13,49 @@ def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
class PropagationFacade:
''' Change propagation API. '''
@classmethod
def after_create_cst(cls, new_cst: list[Constituenta], source: RSForm) -> None:
@staticmethod
def after_create_cst(new_cst: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions when new constituent is created. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).after_create_cst(new_cst, source)
OperationSchema(host).after_create_cst(new_cst, source)
@classmethod
def after_change_cst_type(cls, target: Constituenta, source: RSForm) -> None:
@staticmethod
def after_change_cst_type(target: Constituenta, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).after_change_cst_type(target, source)
OperationSchema(host).after_change_cst_type(target, source)
@classmethod
def after_update_cst(cls, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
@staticmethod
def after_update_cst(target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None:
''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model)
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
def before_delete(cls, target: list[Constituenta], source: RSForm) -> None:
@staticmethod
def before_delete_cst(target: list[Constituenta], source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(source.model)
for host in hosts:
ChangeManager(host).before_delete(target, source)
OperationSchema(host).before_delete_cst(target, source)
@classmethod
def before_substitute(cls, substitutions: list[tuple[Constituenta, Constituenta]], source: RSForm) -> None:
@staticmethod
def before_substitute(substitutions: CstSubstitution, source: RSForm) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
hosts = _get_oss_hosts(source.model)
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. '''
from .Argument import Argument
from .ChangeManager import ChangeManager
from .Inheritance import Inheritance
from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .Substitution import Substitution
from .PropagationFacade import PropagationFacade
from .Substitution import Substitution

View File

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

View File

@ -138,6 +138,26 @@ class OperationTargetSerializer(serializers.Serializer):
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):
''' Serializer: Set input schema for operation. '''
target = PKField(many=False, queryset=Operation.objects.all())

View File

@ -1,4 +1,5 @@
''' Tests for REST API OSS propagation. '''
from .t_attributes import *
from .t_constituents import *
from .t_operations 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.ks4D2.definition_formal, r'X1 X2 X3 X2 D1')
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):
self.populateData()
self.operation1.result = None
self.operation1.save()
data = {
'item_data': {
'alias': 'Test4',
@ -223,11 +226,14 @@ class TestOssViewset(EndpointTester):
'alias': 'Test4',
'title': 'Test title',
'comment': 'Comment',
'operation_type': OperationType.INPUT
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'create_schema': True,
'positions': [],
}
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['result'] = None
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
@ -309,8 +315,6 @@ class TestOssViewset(EndpointTester):
data['target'] = self.operation1.pk
data['input'] = None
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
@ -343,9 +347,29 @@ class TestOssViewset(EndpointTester):
'target': self.operation1.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.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')
def test_update_operation(self):

View File

@ -1,7 +1,8 @@
''' Endpoints for OSS. '''
from typing import cast
from typing import Optional, cast
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, serializers
@ -109,6 +110,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
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']:
oss.create_input(new_operation)
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(
summary='delete operation',
tags=['OSS'],
request=s.OperationTargetSerializer,
request=s.OperationDeleteSerializer,
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
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')
def delete_operation(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete operation. '''
serializer = s.OperationTargetSerializer(
serializer = s.OperationDeleteSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
operation = cast(m.Operation, serializer.validated_data['target'])
old_schema: Optional[LibraryItem] = operation.result
with transaction.atomic():
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(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
@ -217,11 +241,28 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
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())
old_schema: Optional[LibraryItem] = target_operation.result
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.set_input(operation, serializer.validated_data['input'])
oss.set_input(target_operation.pk, schema)
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data

View File

@ -29,71 +29,9 @@ DELETED_ALIAS = 'DEL'
class RSForm:
''' 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):
self.model = model
self.cache: RSForm.Cache = RSForm.Cache(self)
self.cache: RSFormCache = RSFormCache(self)
@staticmethod
def create(**kwargs) -> 'RSForm':
@ -107,6 +45,18 @@ class RSForm:
model = LibraryItem.objects.get(pk=pk)
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:
''' Model wrapper. '''
self.model.save(*args, **kwargs)
@ -138,7 +88,7 @@ class RSForm:
''' Access semantic information on constituents. '''
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. '''
self.cache.ensure_loaded()
graph_terms = self._graph_term()
@ -209,7 +159,7 @@ class RSForm:
result.save()
self.cache.insert(result)
self.on_term_change([result.pk])
self.after_term_change([result.pk])
result.refresh_from_db()
return result
@ -239,8 +189,12 @@ class RSForm:
self.save(update_fields=['time_update'])
return result
def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST,
initial_mapping: Optional[dict[str, str]] = None) -> list[Constituenta]:
def insert_copy(
self,
items: list[Constituenta],
position: int = INSERT_LAST,
initial_mapping: Optional[dict[str, str]] = None
) -> list[Constituenta]:
''' Insert copy of target constituents updating references. '''
count = len(items)
if count == 0:
@ -252,10 +206,12 @@ class RSForm:
indices: dict[str, int] = {}
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 {}
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
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
mapping[cst.alias] = newAlias
@ -318,7 +274,7 @@ class RSForm:
cst.definition_resolved = resolver.resolve(cst.definition_raw)
cst.save()
if term_changed:
self.on_term_change([cst.pk])
self.after_term_change([cst.pk])
self.save(update_fields=['time_update'])
return old_data
@ -370,7 +326,7 @@ class RSForm:
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
self._reset_order()
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:
''' Restore order based on types and term graph. '''
@ -382,19 +338,6 @@ class RSForm:
mapping = self._create_reset_mapping()
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:
''' Change type of constituenta generating alias automatically. '''
self.cache.ensure_loaded()
@ -419,6 +362,17 @@ class RSForm:
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
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:
''' Trigger reference resolution for all texts. '''
self.cache.ensure_loaded()
@ -482,6 +436,19 @@ class RSForm:
self.save(update_fields=['time_update'])
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:
if shift == 0:
return
@ -561,6 +528,68 @@ class RSForm:
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:
''' Semantic information derived from constituents. '''

View File

@ -388,7 +388,7 @@ class TestRSForm(DBTester):
x1.term_resolved = 'слон'
x1.save()
self.schema.on_term_change([x1.pk])
self.schema.after_term_change([x1.pk])
x1.refresh_from_db()
x2.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']
schema = m.RSForm(model)
with transaction.atomic():
PropagationFacade.before_delete(cst_list, schema)
PropagationFacade.before_delete_cst(cst_list, schema)
schema.delete_cst(cst_list)
return Response(
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)
replacement = new_items[index]
substitutions.append((original, replacement))
receiver.substitute(substitutions)
# TODO: propagate substitutions
PropagationFacade.before_substitute(substitutions, receiver)
receiver.substitute(substitutions)
receiver.restore_order()

View File

@ -1,8 +1,8 @@
tzdata==2024.1
Django==5.0.7
Django==5.1
djangorestframework==3.15.2
django-cors-headers==4.4.0
django-filter==24.2
django-filter==24.3
drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3
@ -11,4 +11,4 @@ cctext==0.1.4
pyconcept==0.1.6
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}'
def operationResultFromAnotherOSS():
return 'Схема является результатом другой ОСС'
def operationInputAlreadyConnected():
return 'Схема уже подключена к другой операции'
def operationNotSynthesis(title: str):
return f'Операция не является Синтезом: {title}'

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
IInputCreatedResponse,
IOperationCreateData,
IOperationCreatedResponse,
IOperationDeleteData,
IOperationSchemaData,
IOperationSetInputData,
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({
endpoint: `/api/oss/${oss}/delete-operation`,
request: request

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,10 +31,6 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
setSelected(newValue);
}, []);
function handleSubmit() {
onSubmit(selected);
}
return (
<Modal
overflowVisible
@ -42,7 +38,7 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
submitText='Подтвердить выбор'
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
onSubmit={() => onSubmit(selected)}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')}
>
<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) : '');
}, []);
function handleSubmit() {
onChangeLocation(location);
}
return (
<Modal
overflowVisible
@ -46,7 +42,7 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.location_len}`}
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
onSubmit={() => onChangeLocation(location)}
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')}
>
<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);
}, [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 data: IOperationUpdateData = {
target: target.id,

View File

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

View File

@ -15,17 +15,12 @@ interface DlgGraphParamsProps extends Pick<ModalProps, 'hideWindow'> {
function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) {
const [params, updateParams] = usePartialUpdate(initial);
function handleSubmit() {
hideWindow();
onConfirm(params);
}
return (
<Modal
canSubmit
hideWindow={hideWindow}
header='Настройки графа термов'
onSubmit={handleSubmit}
onSubmit={() => onConfirm(params)}
submitText='Применить'
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 [cstData, updateData] = usePartialUpdate(initial);
const handleSubmit = () => onRename(cstData);
useLayoutEffect(() => {
if (schema && initial && cstData.cst_type !== initial.cst_type) {
updateData({ alias: generateAlias(cstData.cst_type, schema) });
@ -47,7 +45,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'}
hideWindow={hideWindow}
canSubmit={validated}
onSubmit={handleSubmit}
onSubmit={() => onRename(cstData)}
className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex justify-center items-center')}
>
<SelectSingle

View File

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

View File

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

View File

@ -3,7 +3,7 @@
*/
import { Graph } from './Graph';
import { LibraryItemID } from './library';
import { ILibraryItem, LibraryItemID } from './library';
import {
IOperation,
IOperationSchema,
@ -21,10 +21,12 @@ export class OssLoader {
private oss: IOperationSchemaData;
private graph: Graph = new Graph();
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.items = items;
}
produceOSS(): IOperationSchema {
@ -36,7 +38,7 @@ export class OssLoader {
result.operationByID = this.operationByID;
result.graph = this.graph;
result.schemas = this.schemas;
result.schemas = this.schemaIDs;
result.stats = this.calculateStats();
return result;
}
@ -53,12 +55,14 @@ export class OssLoader {
}
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() {
this.graph.topologicalOrder().forEach(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.arguments = this.oss.arguments
.filter(item => item.operation === operationID)
@ -72,7 +76,7 @@ export class OssLoader {
count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).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;
is_owned: boolean;
substitutions: ICstSubstituteEx[];
arguments: OperationID[];
}
@ -85,6 +86,14 @@ export interface IOperationUpdateData extends ITargetOperation {
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.
*/
@ -119,10 +128,10 @@ export interface ICstSubstituteData {
* Represents substitution for multi synthesis table.
*/
export interface IMultiSubstitution {
original_source: ILibraryItem | undefined;
original: IConstituenta | undefined;
substitution: IConstituenta | undefined;
substitution_source: ILibraryItem | undefined;
original_source: ILibraryItem;
original: IConstituenta;
substitution: IConstituenta;
substitution_source: ILibraryItem;
}
/**

View File

@ -33,6 +33,13 @@ function InputNode(node: OssNodeInternal) {
disabled={!hasFile}
/>
</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'>
{node.data.label}
{controller.showTooltip && !node.dragging ? (

View File

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

View File

@ -33,6 +33,12 @@ function OperationNode(node: OssNodeInternal) {
/>
</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'>
{node.data.label}
{controller.showTooltip && !node.dragging ? (

View File

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

View File

@ -175,7 +175,11 @@ function ToolbarOssGraph({
<MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
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}
/>
</div>

View File

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

View File

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

View File

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