Compare commits

..

9 Commits

Author SHA1 Message Date
Ivan
00934d5716 B: Fix oss filtering and error messages
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-08-18 12:55:41 +03:00
Ivan
dee49b789a M: Minor OSS UI fixes 2024-08-17 23:43:15 +03:00
Ivan
b82c7315b3 R: Restructure help pages 2024-08-17 23:42:17 +03:00
Ivan
c1c8384024 F: Use embed instead of react-pdf
Significantly decreases package size
2024-08-17 23:01:55 +03:00
Ivan
85c3027d3d M: Implement RSForm sorting for OSS and small UI fixes 2024-08-17 22:35:59 +03:00
Ivan
259259ec7e F: Improve OSS UI 2024-08-17 12:16:50 +03:00
Ivan
a97d1bebb9 Update MenuRSTabs.tsx 2024-08-16 21:04:37 +03:00
Ivan
60eba81001 F: Propagate operation changes to OSS 2024-08-16 20:57:07 +03:00
Ivan
3032d90f32 R: preparing change operation propagation 2024-08-16 19:53:01 +03:00
64 changed files with 676 additions and 1023 deletions

View File

@ -37,7 +37,6 @@ This readme file is used mostly to document project dependencies and conventions
- react-intl - react-intl
- react-select - react-select
- react-error-boundary - react-error-boundary
- react-pdf
- react-tooltip - react-tooltip
- react-zoom-pan-pinch - react-zoom-pan-pinch
- reactflow - reactflow

View File

@ -95,14 +95,15 @@ class OperationSchema:
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
return result return result
def delete_operation(self, target: Operation, keep_constituents: bool = False): def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete operation. ''' ''' Delete operation. '''
operation = self.cache.operation_by_id[target]
if not keep_constituents: if not keep_constituents:
schema = self.cache.get_schema(target) schema = self.cache.get_schema(operation)
if schema is not None: if schema is not None:
self.before_delete_cst(schema.cache.constituents, schema) self.before_delete_cst(schema, schema.cache.constituents)
self.cache.remove_operation(target.pk) self.cache.remove_operation(target)
target.delete() operation.delete()
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None: def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
@ -115,7 +116,7 @@ class OperationSchema:
if old_schema is not None: if old_schema is not None:
if has_children: if has_children:
self.before_delete_cst(old_schema.cache.constituents, old_schema) self.before_delete_cst(old_schema, old_schema.cache.constituents)
self.cache.remove_schema(old_schema) self.cache.remove_schema(old_schema)
operation.result = schema operation.result = schema
@ -128,58 +129,75 @@ class OperationSchema:
if schema is not None and has_children: if schema is not None and has_children:
rsform = RSForm(schema) rsform = RSForm(schema)
self.after_create_cst(list(rsform.constituents()), rsform) self.after_create_cst(rsform, list(rsform.constituents()))
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
def set_arguments(self, operation: Operation, arguments: list[Operation]) -> None: def set_arguments(self, target: int, arguments: list[Operation]) -> None:
''' Set arguments to operation. ''' ''' Set arguments to operation. '''
self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target]
processed: list[Operation] = [] processed: list[Operation] = []
changed = False deleted: list[Argument] = []
for current in operation.getArguments(): for current in operation.getArguments():
if current.argument not in arguments: if current.argument not in arguments:
changed = True deleted.append(current)
current.delete()
else: else:
processed.append(current.argument) processed.append(current.argument)
if len(deleted) > 0:
self.before_delete_arguments(operation, [x.argument for x in deleted])
for deleted_arg in deleted:
self.cache.remove_argument(deleted_arg)
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
added: list[Operation] = []
for arg in arguments: for arg in arguments:
if arg not in processed: if arg not in processed:
changed = True
processed.append(arg) processed.append(arg)
Argument.objects.create(operation=operation, argument=arg) new_arg = Argument.objects.create(operation=operation, argument=arg)
if not changed: self.cache.insert_argument(new_arg)
return added.append(arg)
# TODO: trigger on_change effects if len(added) > 0:
self.after_create_arguments(operation, added)
if len(added) > 0 or len(deleted) > 0:
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
def set_substitutions(self, target: Operation, substitutes: list[dict]) -> None: def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
''' Clear all arguments for operation. ''' ''' Clear all arguments for operation. '''
self.cache.ensure_loaded()
operation = self.cache.operation_by_id[target]
schema = self.cache.get_schema(operation)
processed: list[dict] = [] processed: list[dict] = []
changed = False deleted: list[Substitution] = []
for current in operation.getSubstitutions():
for current in target.getSubstitutions():
subs = [ subs = [
x for x in substitutes x for x in substitutes
if x['original'] == current.original and x['substitution'] == current.substitution if x['original'] == current.original and x['substitution'] == current.substitution
] ]
if len(subs) == 0: if len(subs) == 0:
changed = True deleted.append(current)
current.delete()
else: else:
processed.append(subs[0]) processed.append(subs[0])
if len(deleted) > 0:
if schema is not None:
for sub in deleted:
self._undo_substitution(schema, sub)
else:
for sub in deleted:
self.cache.remove_substitution(sub)
Substitution.objects.filter(pk__in=[x.pk for x in deleted]).delete()
for sub in substitutes: added: list[Substitution] = []
if sub not in processed: for sub_item in substitutes:
changed = True if sub_item not in processed:
Substitution.objects.create( new_sub = Substitution.objects.create(
operation=target, operation=operation,
original=sub['original'], original=sub_item['original'],
substitution=sub['substitution'] substitution=sub_item['substitution']
) )
added.append(new_sub)
self._process_added_substitutions(schema, added)
if not changed: if len(added) > 0 or len(deleted) > 0:
return
# TODO: trigger on_change effects
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
def create_input(self, operation: Operation) -> RSForm: def create_input(self, operation: Operation) -> RSForm:
@ -240,9 +258,9 @@ class OperationSchema:
self.save(update_fields=['time_update']) self.save(update_fields=['time_update'])
return True return True
def after_create_cst(self, cst_list: list[Constituenta], source: RSForm) -> None: def after_create_cst(self, source: RSForm, cst_list: list[Constituenta]) -> None:
''' Trigger cascade resolutions when new constituent is created. ''' ''' Trigger cascade resolutions when new constituent is created. '''
self.cache.insert(source) self.cache.insert_schema(source)
inserted_aliases = [cst.alias for cst in cst_list] inserted_aliases = [cst.alias for cst in cst_list]
depend_aliases: set[str] = set() depend_aliases: set[str] = set()
for new_cst in cst_list: for new_cst in cst_list:
@ -254,17 +272,17 @@ class OperationSchema:
if cst is not None: if cst is not None:
alias_mapping[alias] = cst alias_mapping[alias] = cst
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.model.pk)
self._cascade_create_cst(cst_list, operation, alias_mapping) self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping)
def after_change_cst_type(self, target: Constituenta, source: RSForm) -> None: def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None:
''' Trigger cascade resolutions when constituenta type is changed. ''' ''' Trigger cascade resolutions when constituenta type is changed. '''
self.cache.insert(source) self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.model.pk)
self._cascade_change_cst_type(target.pk, target.cst_type, operation.pk) self._cascade_change_cst_type(operation.pk, target.pk, target.cst_type)
def after_update_cst(self, target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None: def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
''' Trigger cascade resolutions when constituenta data is changed. ''' ''' Trigger cascade resolutions when constituenta data is changed. '''
self.cache.insert(source) self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.model.pk)
depend_aliases = self._extract_data_references(data, old_data) depend_aliases = self._extract_data_references(data, old_data)
alias_mapping: CstMapping = {} alias_mapping: CstMapping = {}
@ -273,56 +291,94 @@ class OperationSchema:
if cst is not None: if cst is not None:
alias_mapping[alias] = cst alias_mapping[alias] = cst
self._cascade_update_cst( self._cascade_update_cst(
cst_id=target.pk,
operation=operation.pk, operation=operation.pk,
cst_id=target.pk,
data=data, data=data,
old_data=old_data, old_data=old_data,
mapping=alias_mapping mapping=alias_mapping
) )
def before_delete_cst(self, target: list[Constituenta], source: RSForm) -> None: def before_delete_cst(self, source: RSForm, target: list[Constituenta]) -> None:
''' Trigger cascade resolutions before constituents are deleted. ''' ''' Trigger cascade resolutions before constituents are deleted. '''
self.cache.insert(source) self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.model.pk)
self._cascade_before_delete(target, operation.pk) self._cascade_delete_inherited(operation.pk, target)
def before_substitute(self, substitutions: CstSubstitution, source: RSForm) -> None: def before_substitute(self, source: RSForm, substitutions: CstSubstitution) -> None:
''' Trigger cascade resolutions before constituents are substituted. ''' ''' Trigger cascade resolutions before constituents are substituted. '''
self.cache.insert(source) self.cache.insert_schema(source)
operation = self.cache.get_operation(source.model.pk) operation = self.cache.get_operation(source.model.pk)
self._cascade_before_substitute(substitutions, operation) self._cascade_before_substitute(substitutions, operation)
def _cascade_create_cst(self, cst_list: list[Constituenta], operation: Operation, mapping: CstMapping) -> None: def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
children = self.cache.graph.outputs[operation.pk] ''' Trigger cascade resolutions before arguments are deleted. '''
if target.result_id is None:
return
for argument in arguments:
parent_schema = self.cache.get_schema(argument)
if parent_schema is not None:
self._execute_delete_inherited(target.pk, parent_schema.cache.constituents)
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
''' Trigger cascade resolutions after arguments are created. '''
schema = self.cache.get_schema(target)
if schema is None:
return
for argument in arguments:
parent_schema = self.cache.get_schema(argument)
if parent_schema is None:
continue
self._execute_inherit_cst(
target_operation=target.pk,
source=parent_schema,
items=list(parent_schema.constituents()),
mapping={}
)
def _cascade_inherit_cst(
self,
target_operation: int,
source: RSForm,
items: list[Constituenta],
mapping: CstMapping
) -> None:
children = self.cache.graph.outputs[target_operation]
if len(children) == 0: if len(children) == 0:
return return
source_schema = self.cache.get_schema(operation)
assert source_schema is not None
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] self._execute_inherit_cst(child_id, source, items, mapping)
child_schema = self.cache.get_schema(child_operation)
if child_schema is None: def _execute_inherit_cst(
continue self,
target_operation: int,
source: RSForm,
items: list[Constituenta],
mapping: CstMapping
) -> None:
operation = self.cache.operation_by_id[target_operation]
destination = self.cache.get_schema(operation)
if destination is None:
return
# TODO: update substitutions for diamond synthesis (if needed) # TODO: update substitutions for diamond synthesis (if needed)
self.cache.ensure_loaded() self.cache.ensure_loaded()
new_mapping = self._transform_mapping(mapping, child_operation, child_schema) new_mapping = self._transform_mapping(mapping, operation, destination)
alias_mapping = OperationSchema._produce_alias_mapping(new_mapping) alias_mapping = OperationSchema._produce_alias_mapping(new_mapping)
insert_where = self._determine_insert_position(cst_list[0], child_operation, source_schema, child_schema) insert_where = self._determine_insert_position(items[0], operation, source, destination)
new_cst_list = child_schema.insert_copy(cst_list, insert_where, alias_mapping) new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
for index, cst in enumerate(new_cst_list): for index, cst in enumerate(new_cst_list):
new_inheritance = Inheritance.objects.create( new_inheritance = Inheritance.objects.create(
operation=child_operation, operation=operation,
child=cst, child=cst,
parent=cst_list[index] parent=items[index]
) )
self.cache.insert_inheritance(new_inheritance) self.cache.insert_inheritance(new_inheritance)
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_create_cst(new_cst_list, child_operation, new_mapping) self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
def _cascade_change_cst_type(self, cst_id: int, ctype: CstType, operation: int) -> None: def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
children = self.cache.graph.outputs[operation] children = self.cache.graph.outputs[operation_id]
if len(children) == 0: if len(children) == 0:
return return
self.cache.ensure_loaded() self.cache.ensure_loaded()
@ -332,13 +388,16 @@ class OperationSchema:
if successor_id is None: if successor_id is None:
continue continue
child_schema = self.cache.get_schema(child_operation) child_schema = self.cache.get_schema(child_operation)
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype): if child_schema is None:
self._cascade_change_cst_type(successor_id, ctype, child_id) continue
if child_schema.change_cst_type(successor_id, ctype):
self._cascade_change_cst_type(child_id, successor_id, ctype)
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def _cascade_update_cst( def _cascade_update_cst(
self, self,
cst_id: int, operation: int, operation: int,
cst_id: int,
data: dict, old_data: dict, data: dict, old_data: dict,
mapping: CstMapping mapping: CstMapping
) -> None: ) -> None:
@ -366,30 +425,33 @@ class OperationSchema:
continue continue
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()} new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self._cascade_update_cst( self._cascade_update_cst(
cst_id=successor_id,
operation=child_id, operation=child_id,
cst_id=successor_id,
data=new_data, data=new_data,
old_data=new_old_data, old_data=new_old_data,
mapping=new_mapping mapping=new_mapping
) )
def _cascade_before_delete(self, target: list[Constituenta], operation: int) -> None: def _cascade_delete_inherited(self, operation: int, target: list[Constituenta]) -> None:
children = self.cache.graph.outputs[operation] children = self.cache.graph.outputs[operation]
if len(children) == 0: if len(children) == 0:
return return
self.cache.ensure_loaded() self.cache.ensure_loaded()
for child_id in children: for child_id in children:
child_operation = self.cache.operation_by_id[child_id] self._execute_delete_inherited(child_id, target)
child_schema = self.cache.get_schema(child_operation)
if child_schema is None: def _execute_delete_inherited(self, operation_id: int, parent_cst: list[Constituenta]) -> None:
continue operation = self.cache.operation_by_id[operation_id]
self._undo_substitutions_cst(target, child_operation, child_schema) schema = self.cache.get_schema(operation)
child_target_ids = self.cache.get_inheritors_list([cst.pk for cst in target], child_id) if schema is None:
child_target_cst = [child_schema.cache.by_id[cst_id] for cst_id in child_target_ids] return
self._cascade_before_delete(child_target_cst, child_id) self._undo_substitutions_cst(parent_cst, operation, schema)
if len(child_target_cst) > 0: target_ids = self.cache.get_inheritors_list([cst.pk for cst in parent_cst], operation_id)
self.cache.remove_cst(child_target_ids, child_id) target_cst = [schema.cache.by_id[cst_id] for cst_id in target_ids]
child_schema.delete_cst(child_target_cst) self._cascade_delete_inherited(operation_id, target_cst)
if len(target_cst) > 0:
self.cache.remove_cst(operation_id, target_ids)
schema.delete_cst(target_cst)
def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None: def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
children = self.cache.graph.outputs[operation.pk] children = self.cache.graph.outputs[operation.pk]
@ -411,12 +473,12 @@ class OperationSchema:
self, self,
mapping: CstMapping, mapping: CstMapping,
target: list[int], target: list[int],
operation: Operation, operation: int,
schema: RSForm schema: RSForm
) -> None: ) -> None:
alias_mapping = OperationSchema._produce_alias_mapping(mapping) alias_mapping = OperationSchema._produce_alias_mapping(mapping)
schema.apply_partial_mapping(alias_mapping, target) schema.apply_partial_mapping(alias_mapping, target)
children = self.cache.graph.outputs[operation.pk] children = self.cache.graph.outputs[operation]
if len(children) == 0: if len(children) == 0:
return return
self.cache.ensure_loaded() self.cache.ensure_loaded()
@ -431,7 +493,7 @@ class OperationSchema:
new_target = self.cache.get_inheritors_list(target, child_id) new_target = self.cache.get_inheritors_list(target, child_id)
if len(new_target) == 0: if len(new_target) == 0:
continue continue
self._cascade_partial_mapping(new_mapping, new_target, child_operation, child_schema) self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema)
@staticmethod @staticmethod
def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]: def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]:
@ -555,37 +617,66 @@ class OperationSchema:
if sub.original_id in target_ids or sub.substitution_id in target_ids: if sub.original_id in target_ids or sub.substitution_id in target_ids:
to_process.append(sub) to_process.append(sub)
for sub in to_process: for sub in to_process:
self._undo_substitution(sub, schema, target_ids) self._undo_substitution(schema, sub, target_ids)
def _undo_substitution(self, target: Substitution, schema: RSForm, ignore_parents: list[int]) -> None: def _undo_substitution(
operation = self.cache.operation_by_id[target.operation_id] self,
schema: RSForm,
target: Substitution,
ignore_parents: Optional[list[int]] = None
) -> None:
if ignore_parents is None:
ignore_parents = []
operation_id = target.operation_id
original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target) original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target)
dependant = [] dependant = []
for cst_id in original_schema.get_dependant([original_cst.pk]): for cst_id in original_schema.get_dependant([original_cst.pk]):
if cst_id not in ignore_parents: if cst_id not in ignore_parents:
inheritor_id = self.cache.get_inheritor(cst_id, operation.pk) inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
if inheritor_id is not None: if inheritor_id is not None:
dependant.append(inheritor_id) dependant.append(inheritor_id)
self.cache.substitutions[operation.pk].remove(target) self.cache.substitutions[operation_id].remove(target)
target.delete() target.delete()
new_original: Optional[Constituenta] = None new_original: Optional[Constituenta] = None
if original_cst.pk not in ignore_parents: if original_cst.pk not in ignore_parents:
full_cst = Constituenta.objects.get(pk=original_cst.pk) full_cst = Constituenta.objects.get(pk=original_cst.pk)
self.after_create_cst([full_cst], original_schema) self.after_create_cst(original_schema, [full_cst])
new_original_id = self.cache.get_inheritor(original_cst.pk, operation.pk) new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id)
assert new_original_id is not None assert new_original_id is not None
new_original = schema.cache.by_id[new_original_id] new_original = schema.cache.by_id[new_original_id]
if len(dependant) == 0: if len(dependant) == 0:
return return
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation.pk) substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id)
assert substitution_id is not None assert substitution_id is not None
substitution_inheritor = schema.cache.by_id[substitution_id] substitution_inheritor = schema.cache.by_id[substitution_id]
mapping = {cast(str, substitution_inheritor.alias): new_original} mapping = {cast(str, substitution_inheritor.alias): new_original}
self._cascade_partial_mapping(mapping, dependant, operation, schema) self._cascade_partial_mapping(mapping, dependant, operation_id, schema)
def _process_added_substitutions(self, schema: Optional[RSForm], added: list[Substitution]) -> None:
if len(added) == 0:
return
if schema is None:
for sub in added:
self.cache.insert_substitution(sub)
return
cst_mapping: CstSubstitution = []
for sub in added:
original_id = self.cache.get_inheritor(sub.original_id, sub.operation_id)
substitution_id = self.cache.get_inheritor(sub.substitution_id, sub.operation_id)
if original_id is None or substitution_id is None:
raise ValueError('Substitutions not found.')
original_cst = schema.cache.by_id[original_id]
substitution_cst = schema.cache.by_id[substitution_id]
cst_mapping.append((original_cst, substitution_cst))
self.before_substitute(schema, cst_mapping)
schema.substitute(cst_mapping)
for sub in added:
self.cache.insert_substitution(sub)
class OssCache: class OssCache:
@ -608,11 +699,18 @@ class OssCache:
self.substitutions: dict[int, list[Substitution]] = {} self.substitutions: dict[int, list[Substitution]] = {}
self.inheritance: dict[int, list[Inheritance]] = {} self.inheritance: dict[int, list[Inheritance]] = {}
def insert(self, schema: RSForm) -> None: def ensure_loaded(self) -> None:
''' Insert new schema. ''' ''' Ensure cache is fully loaded. '''
if not self._schema_by_id.get(schema.model.pk): if self.is_loaded:
schema.cache.ensure_loaded() return
self._insert_new(schema) 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_schema(self, operation: Operation) -> Optional[RSForm]: def get_schema(self, operation: Operation) -> Optional[RSForm]:
''' Get schema by Operation. ''' ''' Get schema by Operation. '''
@ -633,19 +731,6 @@ class OssCache:
return operation return operation
raise ValueError(f'Operation for schema {schema} not found') 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]: def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
''' Get child for parent inside target RSFrom. ''' ''' Get child for parent inside target RSFrom. '''
for item in self.inheritance[operation]: for item in self.inheritance[operation]:
@ -668,6 +753,12 @@ class OssCache:
return self.get_inheritor(sub.substitution_id, operation) return self.get_inheritor(sub.substitution_id, operation)
return self.get_inheritor(parent_cst, operation) return self.get_inheritor(parent_cst, operation)
def insert_schema(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 insert_operation(self, operation: Operation) -> None: def insert_operation(self, operation: Operation) -> None:
''' Insert new operation. ''' ''' Insert new operation. '''
self.operations.append(operation) self.operations.append(operation)
@ -677,6 +768,10 @@ class OssCache:
self.substitutions[operation.pk] = [] self.substitutions[operation.pk] = []
self.inheritance[operation.pk] = [] self.inheritance[operation.pk] = []
def insert_argument(self, argument: Argument) -> None:
''' Insert new argument. '''
self.graph.add_edge(argument.operation_id, argument.argument_id)
def insert_inheritance(self, inheritance: Inheritance) -> None: def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. ''' ''' Insert new inheritance. '''
self.inheritance[inheritance.operation_id].append(inheritance) self.inheritance[inheritance.operation_id].append(inheritance)
@ -685,7 +780,7 @@ class OssCache:
''' Insert new substitution. ''' ''' Insert new substitution. '''
self.substitutions[sub.operation_id].append(sub) self.substitutions[sub.operation_id].append(sub)
def remove_cst(self, target: list[int], operation: int) -> None: def remove_cst(self, operation: int, target: list[int]) -> None:
''' Remove constituents from operation. ''' ''' Remove constituents from operation. '''
subs_to_delete = [ subs_to_delete = [
sub for sub in self.substitutions[operation] sub for sub in self.substitutions[operation]
@ -697,9 +792,15 @@ class OssCache:
for item in inherit_to_delete: for item in inherit_to_delete:
self.inheritance[operation].remove(item) self.inheritance[operation].remove(item)
def remove_schema(self, schema: RSForm) -> None:
''' Remove schema from cache. '''
self._schemas.remove(schema)
del self._schema_by_id[schema.model.pk]
def remove_operation(self, operation: int) -> None: def remove_operation(self, operation: int) -> None:
''' Remove operation from cache. ''' ''' Remove operation from cache. '''
target = self.operation_by_id[operation] target = self.operation_by_id[operation]
self.graph.remove_node(operation)
if target.result_id in self._schema_by_id: if target.result_id in self._schema_by_id:
self._schemas.remove(self._schema_by_id[target.result_id]) self._schemas.remove(self._schema_by_id[target.result_id])
del self._schema_by_id[target.result_id] del self._schema_by_id[target.result_id]
@ -709,10 +810,13 @@ class OssCache:
del self.substitutions[operation] del self.substitutions[operation]
del self.inheritance[operation] del self.inheritance[operation]
def remove_schema(self, schema: RSForm) -> None: def remove_argument(self, argument: Argument) -> None:
''' Remove schema from cache. ''' ''' Remove argument from cache. '''
self._schemas.remove(schema) self.graph.remove_edge(argument.operation_id, argument.argument_id)
del self._schema_by_id[schema.model.pk]
def remove_substitution(self, target: Substitution) -> None:
''' Remove substitution from cache. '''
self.substitutions[target.operation_id].remove(target)
def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]: def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
operation = self.operation_by_id[sub.operation_id] operation = self.operation_by_id[sub.operation_id]

View File

@ -14,39 +14,39 @@ class PropagationFacade:
''' Change propagation API. ''' ''' Change propagation API. '''
@staticmethod @staticmethod
def after_create_cst(new_cst: list[Constituenta], source: RSForm) -> None: def after_create_cst(source: RSForm, new_cst: list[Constituenta]) -> None:
''' Trigger cascade resolutions when new constituent is created. ''' ''' Trigger cascade resolutions when new constituent is created. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
OperationSchema(host).after_create_cst(new_cst, source) OperationSchema(host).after_create_cst(source, new_cst)
@staticmethod @staticmethod
def after_change_cst_type(target: Constituenta, source: RSForm) -> None: def after_change_cst_type(source: RSForm, target: Constituenta) -> None:
''' Trigger cascade resolutions when constituenta type is changed. ''' ''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
OperationSchema(host).after_change_cst_type(target, source) OperationSchema(host).after_change_cst_type(source, target)
@staticmethod @staticmethod
def after_update_cst(target: Constituenta, data: dict, old_data: dict, source: RSForm) -> None: def after_update_cst(source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
''' Trigger cascade resolutions when constituenta data is changed. ''' ''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
OperationSchema(host).after_update_cst(target, data, old_data, source) OperationSchema(host).after_update_cst(source, target, data, old_data)
@staticmethod @staticmethod
def before_delete_cst(target: list[Constituenta], source: RSForm) -> None: def before_delete_cst(source: RSForm, target: list[Constituenta]) -> None:
''' Trigger cascade resolutions before constituents are deleted. ''' ''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
OperationSchema(host).before_delete_cst(target, source) OperationSchema(host).before_delete_cst(source, target)
@staticmethod @staticmethod
def before_substitute(substitutions: CstSubstitution, source: RSForm) -> None: def before_substitute(source: RSForm, substitutions: CstSubstitution) -> None:
''' Trigger cascade resolutions before constituents are substituted. ''' ''' Trigger cascade resolutions before constituents are substituted. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model)
for host in hosts: for host in hosts:
OperationSchema(host).before_substitute(substitutions, source) OperationSchema(host).before_substitute(source, substitutions)
@staticmethod @staticmethod
def before_delete_schema(item: LibraryItem) -> None: def before_delete_schema(item: LibraryItem) -> None:
@ -58,4 +58,4 @@ class PropagationFacade:
return return
schema = RSForm(item) schema = RSForm(item)
PropagationFacade.before_delete_cst(list(schema.constituents()), schema) PropagationFacade.before_delete_cst(schema, list(schema.constituents()))

View File

@ -51,7 +51,7 @@ class TestChangeConstituents(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.execute_operation(self.operation3) self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
self.ks3 = RSForm(self.operation3.result) self.ks3 = RSForm(self.operation3.result)

View File

@ -71,8 +71,8 @@ class TestChangeOperations(EndpointTester):
alias='4', alias='4',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation4, [{ self.owned.set_substitutions(self.operation4.pk, [{
'original': self.ks1X1, 'original': self.ks1X1,
'substitution': self.ks2S1 'substitution': self.ks2S1
}]) }])
@ -92,8 +92,8 @@ class TestChangeOperations(EndpointTester):
alias='5', alias='5',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5, [{ self.owned.set_substitutions(self.operation5.pk, [{
'original': self.ks4X1, 'original': self.ks4X1,
'substitution': self.ks3X1 'substitution': self.ks3X1
}]) }])
@ -249,3 +249,75 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituents().count(), 8)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_substitutions(self):
data = {
'target': self.operation4.pk,
'item_data': {
'alias': 'Test4 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
'substitutions': [
{
'original': self.ks1X1.pk,
'substitution': self.ks2X2.pk
},
{
'original': self.ks2X1.pk,
'substitution': self.ks1D1.pk
}
]
}
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(), 2)
subs3_4 = self.operation5.getSubstitutions()
self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 5)
self.assertEqual(self.ks5.constituents().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 D2 X3 S1 D1 D2 D3')
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_change_arguments(self):
data = {
'target': self.operation4.pk,
'item_data': {
'alias': 'Test4 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk],
}
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(), 4)
self.assertEqual(self.ks5.constituents().count(), 6)
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')
data['arguments'] = [self.operation1.pk, self.operation2.pk]
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(), 7)
self.assertEqual(self.ks5.constituents().count(), 9)
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')

View File

@ -71,8 +71,8 @@ class TestChangeSubstitutions(EndpointTester):
alias='4', alias='4',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation4, [self.operation1, self.operation2]) self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation4, [{ self.owned.set_substitutions(self.operation4.pk, [{
'original': self.ks1X1, 'original': self.ks1X1,
'substitution': self.ks2S1 'substitution': self.ks2S1
}]) }])
@ -92,8 +92,8 @@ class TestChangeSubstitutions(EndpointTester):
alias='5', alias='5',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation5, [self.operation4, self.operation3]) self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5, [{ self.owned.set_substitutions(self.operation5.pk, [{
'original': self.ks4X1, 'original': self.ks4X1,
'substitution': self.ks3X1 'substitution': self.ks3X1
}]) }])

View File

@ -55,8 +55,8 @@ class TestOssViewset(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
self.owned.set_substitutions(self.operation3, [{ self.owned.set_substitutions(self.operation3.pk, [{
'original': self.ks1X1, 'original': self.ks1X1,
'substitution': self.ks2X1 'substitution': self.ks2X1
}]) }])

View File

@ -129,7 +129,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.create_input(new_operation) oss.create_input(new_operation)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
oss.set_arguments( oss.set_arguments(
operation=new_operation, target=new_operation.pk,
arguments=serializer.validated_data['arguments'] arguments=serializer.validated_data['arguments']
) )
return Response( return Response(
@ -165,7 +165,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
old_schema: Optional[LibraryItem] = operation.result old_schema: Optional[LibraryItem] = operation.result
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(operation, serializer.validated_data['keep_constituents']) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
if old_schema is not None: if old_schema is not None:
if serializer.validated_data['delete_schema']: if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema) m.PropagationFacade.before_delete_schema(old_schema)
@ -305,9 +305,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
operation.result.comment = operation.comment operation.result.comment = operation.comment
operation.result.save() operation.result.save()
if 'arguments' in serializer.validated_data: if 'arguments' in serializer.validated_data:
oss.set_arguments(operation, serializer.validated_data['arguments']) oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data: if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation, serializer.validated_data['substitutions']) oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data

View File

@ -42,6 +42,28 @@ class Graph(Generic[ItemType]):
if src not in self.inputs[dest]: if src not in self.inputs[dest]:
self.inputs[dest].append(src) self.inputs[dest].append(src)
def remove_edge(self, src: ItemType, dest: ItemType):
''' Remove edge from graph. '''
if not self.contains(src) or not self.contains(dest):
return
if dest in self.outputs[src]:
self.outputs[src].remove(dest)
if src in self.inputs[dest]:
self.inputs[dest].remove(src)
def remove_node(self, target: ItemType):
''' Remove node from graph. '''
if not self.contains(target):
return
del self.outputs[target]
del self.inputs[target]
for list_out in self.outputs.values():
if target in list_out:
list_out.remove(target)
for list_in in self.inputs.values():
if target in list_in:
list_in.remove(target)
def expand_inputs(self, origin: Iterable[ItemType]) -> list[ItemType]: def expand_inputs(self, origin: Iterable[ItemType]) -> list[ItemType]:
''' Expand origin nodes forward through graph edges. ''' ''' Expand origin nodes forward through graph edges. '''
result: list[ItemType] = [] result: list[ItemType] = []

View File

@ -26,6 +26,32 @@ class TestGraph(unittest.TestCase):
self.assertTrue(graph.has_edge(1, 3)) self.assertTrue(graph.has_edge(1, 3))
self.assertTrue(graph.has_edge(2, 1)) self.assertTrue(graph.has_edge(2, 1))
def test_remove_node(self):
graph = Graph({
1: [2],
2: [3, 5],
3: [],
5: []
})
self.assertEqual(len(graph.outputs), 4)
graph.remove_node(0)
graph.remove_node(2)
self.assertEqual(graph.outputs[1], [])
self.assertEqual(len(graph.outputs), 3)
def test_remove_edge(self):
graph = Graph({
1: [2],
2: [3, 5],
3: [],
5: []
})
graph.remove_edge(0, 1)
graph.remove_edge(2, 1)
self.assertEqual(graph.outputs[1], [2])
graph.remove_edge(1, 2)
self.assertEqual(graph.outputs[1], [])
graph.remove_edge(1, 2)
def test_expand_outputs(self): def test_expand_outputs(self):
graph = Graph({ graph = Graph({

View File

@ -87,7 +87,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = m.RSForm(self._get_item()) schema = m.RSForm(self._get_item())
with transaction.atomic(): with transaction.atomic():
new_cst = schema.create_cst(data, insert_after) new_cst = schema.create_cst(data, insert_after)
PropagationFacade.after_create_cst([new_cst], schema) PropagationFacade.after_create_cst(schema, [new_cst])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
@ -118,7 +118,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
with transaction.atomic(): with transaction.atomic():
old_data = schema.update_cst(cst, data) old_data = schema.update_cst(cst, data)
PropagationFacade.after_update_cst(cst, data, old_data, schema) PropagationFacade.after_update_cst(schema, cst, data, old_data)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data data=s.CstSerializer(m.Constituenta.objects.get(pk=request.data['target'])).data
@ -159,7 +159,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic(): with transaction.atomic():
new_cst = schema.produce_structure(cst, cst_parse) new_cst = schema.produce_structure(cst, cst_parse)
PropagationFacade.after_create_cst(new_cst, schema) PropagationFacade.after_create_cst(schema, new_cst)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
@ -197,7 +197,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.save() schema.save()
cst.refresh_from_db() cst.refresh_from_db()
if changed_type: if changed_type:
PropagationFacade.after_change_cst_type(cst, schema) PropagationFacade.after_change_cst_type(schema, cst)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
@ -233,7 +233,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(substitutions, schema) PropagationFacade.before_substitute(schema, substitutions)
schema.substitute(substitutions) schema.substitute(substitutions)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -263,7 +263,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst_list: list[m.Constituenta] = serializer.validated_data['items'] cst_list: list[m.Constituenta] = serializer.validated_data['items']
schema = m.RSForm(model) schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
PropagationFacade.before_delete_cst(cst_list, schema) PropagationFacade.before_delete_cst(schema, cst_list)
schema.delete_cst(cst_list) schema.delete_cst(cst_list)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -581,7 +581,7 @@ def inline_synthesis(request: Request) -> HttpResponse:
with transaction.atomic(): with transaction.atomic():
new_items = receiver.insert_copy(items) new_items = receiver.insert_copy(items)
PropagationFacade.after_create_cst(new_items, receiver) PropagationFacade.after_create_cst(receiver, new_items)
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
@ -595,7 +595,7 @@ def inline_synthesis(request: Request) -> HttpResponse:
replacement = new_items[index] replacement = new_items[index]
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(substitutions, receiver) PropagationFacade.before_substitute(receiver, substitutions)
receiver.substitute(substitutions) receiver.substitute(substitutions)
receiver.restore_order() receiver.restore_order()

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,6 @@
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"react-intl": "^6.6.8", "react-intl": "^6.6.8",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-pdf": "^9.1.0",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"react-select": "^5.8.0", "react-select": "^5.8.0",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",

View File

@ -2,7 +2,6 @@
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { pdfjs } from 'react-pdf';
import { AuthState } from '@/context/AuthContext'; import { AuthState } from '@/context/AuthContext';
import { OptionsState } from '@/context/ConceptOptionsContext'; import { OptionsState } from '@/context/ConceptOptionsContext';
@ -12,8 +11,6 @@ import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './ErrorFallback'; import ErrorFallback from './ErrorFallback';
pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const resetState = () => { const resetState = () => {
console.log('Resetting state after error fallback'); console.log('Resetting state after error fallback');
}; };

View File

@ -5,6 +5,7 @@ import { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import { extractErrorMessage } from '@/utils/utils';
import { axiosInstance } from './apiConfiguration'; import { axiosInstance } from './apiConfiguration';
@ -50,7 +51,7 @@ export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosReq
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message); if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
@ -69,7 +70,7 @@ export function AxiosPost<RequestData, ResponseData>({
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message); if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
@ -88,7 +89,7 @@ export function AxiosDelete<RequestData, ResponseData>({
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message); if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
@ -108,7 +109,7 @@ export function AxiosPatch<RequestData, ResponseData>({
}) })
.catch((error: Error | AxiosError) => { .catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message); if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }

View File

@ -64,6 +64,7 @@ export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { TbHexagons as IconOSS } from 'react-icons/tb'; export { TbHexagons as IconOSS } from 'react-icons/tb';
export { TbHexagon as IconRSForm } from 'react-icons/tb'; export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbTopologyRing as IconConsolidation } from 'react-icons/tb';
export { GrInherit as IconChild } from 'react-icons/gr'; export { GrInherit as IconChild } from 'react-icons/gr';
export { RiParentLine as IconParent } from 'react-icons/ri'; export { RiParentLine as IconParent } from 'react-icons/ri';
export { BiSpa as IconPredecessor } from 'react-icons/bi'; export { BiSpa as IconPredecessor } from 'react-icons/bi';

View File

@ -72,6 +72,11 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
<b>КС не принадлежит ОСС</b> <b>КС не принадлежит ОСС</b>
</p> </p>
) : null} ) : null}
{node.data.operation.is_consolidation ? (
<p>
<b>Ромбовидный синтез</b>
</p>
) : null}
{node.data.operation.title ? ( {node.data.operation.title ? (
<p> <p>
<b>Название: </b> <b>Название: </b>

View File

@ -4,9 +4,8 @@ import { useIntl } from 'react-intl';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { ILibraryFilter } from '@/models/miscellaneous'; import { matchLibraryItem } from '@/models/libraryAPI';
import FlexColumn from '../ui/FlexColumn'; import FlexColumn from '../ui/FlexColumn';
@ -15,6 +14,8 @@ interface PickSchemaProps {
initialFilter?: string; initialFilter?: string;
rows?: number; rows?: number;
items: ILibraryItem[];
itemType: LibraryItemType;
value?: LibraryItemID; value?: LibraryItemID;
baseFilter?: (target: ILibraryItem) => boolean; baseFilter?: (target: ILibraryItem) => boolean;
onSelectValue: (newValue: LibraryItemID) => void; onSelectValue: (newValue: LibraryItemID) => void;
@ -22,31 +23,31 @@ interface PickSchemaProps {
const columnHelper = createColumnHelper<ILibraryItem>(); const columnHelper = createColumnHelper<ILibraryItem>();
function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue, baseFilter }: PickSchemaProps) { function PickSchema({
id,
initialFilter = '',
rows = 4,
items,
itemType,
value,
onSelectValue,
baseFilter
}: PickSchemaProps) {
const intl = useIntl(); const intl = useIntl();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const library = useLibrary();
const [filterText, setFilterText] = useState(initialFilter); const [filterText, setFilterText] = useState(initialFilter);
const [filter, setFilter] = useState<ILibraryFilter>({}); const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
const [items, setItems] = useState<ILibraryItem[]>([]); const baseFiltered = useMemo(
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
[items, itemType, baseFilter]
);
useLayoutEffect(() => { useLayoutEffect(() => {
setFilter({ const newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText));
query: filterText, setFiltered(newFiltered);
type: LibraryItemType.RSFORM
});
}, [filterText]); }, [filterText]);
useLayoutEffect(() => {
const filtered = library.applyFilter(filter);
if (baseFilter) {
setItems(filtered.filter(baseFilter));
} else {
setItems(filtered);
}
}, [library, filter, filter.query, baseFilter]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
@ -106,7 +107,7 @@ function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue, ba
noHeader noHeader
noFooter noFooter
className='text-sm select-none cc-scroll-y' className='text-sm select-none cc-scroll-y'
data={items} data={filtered}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
noDataComponent={ noDataComponent={

View File

@ -1,61 +1,29 @@
'use client'; 'use client';
import type { PDFDocumentProxy } from 'pdfjs-dist'; import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { graphLightT } from '@/styling/color';
import Overlay from '../Overlay'; const MAXIMUM_WIDTH = 1600;
import PageControls from './PageControls';
const MAXIMUM_WIDTH = 1000;
const MINIMUM_WIDTH = 300; const MINIMUM_WIDTH = 300;
interface PDFViewerProps { interface PDFViewerProps {
file?: string | ArrayBuffer | Blob; file?: string;
offsetXpx?: number; offsetXpx?: number;
minWidth?: number; minWidth?: number;
} }
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) { function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { calculateHeight } = useConceptOptions();
const [pageCount, setPageCount] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const pageWidth = useMemo(() => { const pageWidth = useMemo(() => {
return Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH)); return Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH));
}, [windowSize, offsetXpx, minWidth]); }, [windowSize, offsetXpx, minWidth]);
const pageHeight = useMemo(() => calculateHeight('1rem'), [calculateHeight]);
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) { return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />;
setPageCount(numPages);
}
return (
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
>
<Overlay position='top-3 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
<Page
className='overflow-hidden pointer-events-none select-none'
renderTextLayer={false}
renderAnnotationLayer={false}
pageNumber={pageNumber}
width={pageWidth}
canvasBackground={graphLightT.canvas.background}
/>
<Overlay position='bottom-3 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
</Document>
);
} }
export default PDFViewer; export default PDFViewer;

View File

@ -1,51 +0,0 @@
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '@/components/Icons';
interface PageControlsProps {
pageNumber: number;
pageCount: number;
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
}
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
return (
<div className='flex items-center'>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(1)}
disabled={pageNumber < 2}
>
<IconPageFirst size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2}
>
<IconPageLeft size='1.5rem' />
</button>
<div className='px-3 text-nowrap'>
Страница {pageNumber} из {pageCount}
</div>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount}
>
<IconPageRight size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount}
>
<IconPageLast size='1.5rem' />
</button>
</div>
);
}
export default PageControls;

View File

@ -8,8 +8,10 @@ import PickSchema from '@/components/select/PickSchema';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperation, IOperationSchema } from '@/models/oss'; import { IOperation, IOperationSchema } from '@/models/oss';
import { sortItemsForOSS } from '@/models/ossAPI';
interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> { interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema; oss: IOperationSchema;
@ -19,6 +21,8 @@ interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) { function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) {
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined); const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]);
const baseFilter = useCallback( const baseFilter = useCallback(
(item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result, (item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result,
@ -55,6 +59,8 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
</div> </div>
</div> </div>
<PickSchema <PickSchema
items={sortedItems}
itemType={LibraryItemType.RSFORM}
value={selected} // prettier: split-line value={selected} // prettier: split-line
onSelectValue={handleSelectLocation} onSelectValue={handleSelectLocation}
rows={8} rows={8}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
@ -10,8 +10,10 @@ import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperationSchema } from '@/models/oss'; import { IOperationSchema } from '@/models/oss';
import { sortItemsForOSS } from '@/models/ossAPI';
import { limits, patterns } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
interface TabInputOperationProps { interface TabInputOperationProps {
@ -42,6 +44,8 @@ function TabInputOperation({
setCreateSchema setCreateSchema
}: TabInputOperationProps) { }: TabInputOperationProps) {
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]); const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]);
useEffect(() => { useEffect(() => {
if (createSchema) { if (createSchema) {
@ -102,7 +106,9 @@ function TabInputOperation({
</div> </div>
{!createSchema ? ( {!createSchema ? (
<PickSchema <PickSchema
value={attachedID} // prettier: split-line items={sortedItems}
value={attachedID}
itemType={LibraryItemType.RSFORM}
onSelectValue={setAttachedID} onSelectValue={setAttachedID}
rows={8} rows={8}
baseFilter={baseFilter} baseFilter={baseFilter}

View File

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

View File

@ -6,7 +6,7 @@ import PickSchema from '@/components/select/PickSchema';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID, LibraryItemType } from '@/models/library';
interface TabSchemaProps { interface TabSchemaProps {
selected?: LibraryItemID; selected?: LibraryItemID;
@ -33,6 +33,8 @@ function TabSchema({ selected, setSelected }: TabSchemaProps) {
</div> </div>
<PickSchema <PickSchema
id='dlg_schema_picker' // prettier: split lines id='dlg_schema_picker' // prettier: split lines
items={library.items}
itemType={LibraryItemType.RSFORM}
rows={15} rows={15}
value={selected} value={selected}
onSelectValue={setSelected} onSelectValue={setSelected}

View File

@ -62,6 +62,7 @@ export class OssLoader {
this.graph.topologicalOrder().forEach(operationID => { this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!; const operation = this.operationByID.get(operationID)!;
const schema = this.items.find(item => item.id === operation.result); const schema = this.items.find(item => item.id === operation.result);
operation.is_consolidation = this.inferConsolidation(operationID);
operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location); operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location);
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
operation.arguments = this.oss.arguments operation.arguments = this.oss.arguments
@ -70,6 +71,19 @@ export class OssLoader {
}); });
} }
private inferConsolidation(operationID: OperationID): boolean {
const inputs = this.graph.expandInputs([operationID]);
if (inputs.length === 0) {
return false;
}
const ancestors = [...inputs];
inputs.forEach(input => {
ancestors.push(...this.graph.expandAllInputs([input]));
});
const unique = new Set(ancestors);
return unique.size < ancestors.length;
}
private calculateStats(): IOperationSchemaStats { private calculateStats(): IOperationSchemaStats {
const items = this.oss.items; const items = this.oss.items;
return { return {

View File

@ -37,6 +37,7 @@ export interface IOperation {
result: LibraryItemID | null; result: LibraryItemID | null;
is_owned: boolean; is_owned: boolean;
is_consolidation: boolean; // aka 'diamond synthesis'
substitutions: ICstSubstituteEx[]; substitutions: ICstSubstituteEx[];
arguments: OperationID[]; arguments: OperationID[];
} }

View File

@ -4,7 +4,8 @@
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { IOperation } from './oss'; import { ILibraryItem } from './library';
import { IOperation, IOperationSchema } from './oss';
/** /**
* Checks if a given target {@link IOperation} matches the specified query using. * Checks if a given target {@link IOperation} matches the specified query using.
@ -16,3 +17,29 @@ export function matchOperation(target: IOperation, query: string): boolean {
const matcher = new TextMatcher(query); const matcher = new TextMatcher(query);
return matcher.test(target.alias) || matcher.test(target.title); return matcher.test(target.alias) || matcher.test(target.title);
} }
/**
* Sorts library items relevant for the specified {@link IOperationSchema}.
*
* @param oss - The {@link IOperationSchema} to be sorted.
* @param items - The items to be sorted.
*/
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === oss.location);
for (const item of items) {
if (item.visible && item.owner === oss.owner && !result.includes(item)) {
result.push(item);
}
}
for (const item of items) {
if (item.visible && !result.includes(item)) {
result.push(item);
}
}
for (const item of items) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}

View File

@ -148,7 +148,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
{ {
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS, when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
style: { style: {
backgroundColor: colors.bgGreen50 color: colors.fgGreen
} }
} }
], ],

View File

@ -1,39 +1,39 @@
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import HelpConceptOSS from './items/cc/HelpConceptOSS';
import HelpConceptRelations from './items/cc/HelpConceptRelations';
import HelpConceptSynthesis from './items/cc/HelpConceptSynthesis';
import HelpConceptSystem from './items/cc/HelpConceptSystem';
import HelpCstAttributes from './items/cc/HelpCstAttributes';
import HelpAccess from './items/HelpAccess'; import HelpAccess from './items/HelpAccess';
import HelpAPI from './items/HelpAPI';
import HelpConcept from './items/HelpConcept'; import HelpConcept from './items/HelpConcept';
import HelpConceptOSS from './items/HelpConceptOSS';
import HelpConceptRelations from './items/HelpConceptRelations';
import HelpConceptSynthesis from './items/HelpConceptSynthesis';
import HelpConceptSystem from './items/HelpConceptSystem';
import HelpContributors from './items/HelpContributors';
import HelpCstAttributes from './items/HelpCstAttributes';
import HelpCstClass from './items/HelpCstClass';
import HelpCstEditor from './items/HelpCstEditor';
import HelpCstStatus from './items/HelpCstStatus';
import HelpExteor from './items/HelpExteor'; import HelpExteor from './items/HelpExteor';
import HelpFormulaTree from './items/HelpFormulaTree';
import HelpInfo from './items/HelpInfo'; import HelpInfo from './items/HelpInfo';
import HelpInterface from './items/HelpInterface'; import HelpInterface from './items/HelpInterface';
import HelpLibrary from './items/HelpLibrary'; import HelpMain from './items/HelpMain';
import HelpOssGraph from './items/HelpOssGraph';
import HelpPortal from './items/HelpPortal';
import HelpPrivacy from './items/HelpPrivacy';
import HelpRSFormCard from './items/HelpRSFormCard';
import HelpRSFormItems from './items/HelpRSFormItems';
import HelpRSFormMenu from './items/HelpRSFormMenu';
import HelpRSLang from './items/HelpRSLang'; import HelpRSLang from './items/HelpRSLang';
import HelpRSLangCorrect from './items/HelpRSLangCorrect';
import HelpRSLangInterpret from './items/HelpRSLangInterpret';
import HelpRSLangOperations from './items/HelpRSLangOperations';
import HelpRSLangTemplates from './items/HelpRSLangTemplates';
import HelpRSLangTypes from './items/HelpRSLangTypes';
import HelpRules from './items/HelpRules';
import HelpTermGraph from './items/HelpTermGraph';
import HelpTerminologyControl from './items/HelpTerminologyControl'; import HelpTerminologyControl from './items/HelpTerminologyControl';
import HelpVersions from './items/HelpVersions'; import HelpVersions from './items/HelpVersions';
import HelpAPI from './items/info/HelpAPI';
import HelpContributors from './items/info/HelpContributors';
import HelpPrivacy from './items/info/HelpPrivacy';
import HelpRules from './items/info/HelpRules';
import HelpRSLangCorrect from './items/rslang/HelpRSLangCorrect';
import HelpRSLangInterpret from './items/rslang/HelpRSLangInterpret';
import HelpRSLangOperations from './items/rslang/HelpRSLangOperations';
import HelpRSLangTemplates from './items/rslang/HelpRSLangTemplates';
import HelpRSLangTypes from './items/rslang/HelpRSLangTypes';
import HelpCstClass from './items/ui/HelpCstClass';
import HelpCstStatus from './items/ui/HelpCstStatus';
import HelpFormulaTree from './items/ui/HelpFormulaTree';
import HelpLibrary from './items/ui/HelpLibrary';
import HelpOssGraph from './items/ui/HelpOssGraph';
import HelpRSCard from './items/ui/HelpRSCard';
import HelpRSEditor from './items/ui/HelpRSEditor';
import HelpRSGraphTerm from './items/ui/HelpRSGraphTerm';
import HelpRSList from './items/ui/HelpRSList';
import HelpRSMenu from './items/ui/HelpRSMenu';
// PDF Viewer setup // PDF Viewer setup
const OFFSET_X_SMALL = 32; const OFFSET_X_SMALL = 32;
@ -49,15 +49,15 @@ interface TopicPageProps {
function TopicPage({ topic }: TopicPageProps) { function TopicPage({ topic }: TopicPageProps) {
const size = useWindowSize(); const size = useWindowSize();
if (topic === HelpTopic.MAIN) return <HelpPortal />; if (topic === HelpTopic.MAIN) return <HelpMain />;
if (topic === HelpTopic.INTERFACE) return <HelpInterface />; if (topic === HelpTopic.INTERFACE) return <HelpInterface />;
if (topic === HelpTopic.UI_LIBRARY) return <HelpLibrary />; if (topic === HelpTopic.UI_LIBRARY) return <HelpLibrary />;
if (topic === HelpTopic.UI_RS_MENU) return <HelpRSFormMenu />; if (topic === HelpTopic.UI_RS_MENU) return <HelpRSMenu />;
if (topic === HelpTopic.UI_RS_CARD) return <HelpRSFormCard />; if (topic === HelpTopic.UI_RS_CARD) return <HelpRSCard />;
if (topic === HelpTopic.UI_RS_LIST) return <HelpRSFormItems />; if (topic === HelpTopic.UI_RS_LIST) return <HelpRSList />;
if (topic === HelpTopic.UI_RS_EDITOR) return <HelpCstEditor />; if (topic === HelpTopic.UI_RS_EDITOR) return <HelpRSEditor />;
if (topic === HelpTopic.UI_GRAPH_TERM) return <HelpTermGraph />; if (topic === HelpTopic.UI_GRAPH_TERM) return <HelpRSGraphTerm />;
if (topic === HelpTopic.UI_FORMULA_TREE) return <HelpFormulaTree />; if (topic === HelpTopic.UI_FORMULA_TREE) return <HelpFormulaTree />;
if (topic === HelpTopic.UI_CST_STATUS) return <HelpCstStatus />; if (topic === HelpTopic.UI_CST_STATUS) return <HelpCstStatus />;
if (topic === HelpTopic.UI_CST_CLASS) return <HelpCstClass />; if (topic === HelpTopic.UI_CST_CLASS) return <HelpCstClass />;

View File

@ -7,7 +7,7 @@ import { external_urls, prefixes } from '@/utils/constants';
import TopicItem from '../TopicItem'; import TopicItem from '../TopicItem';
function HelpPortal() { function HelpMain() {
return ( return (
<div> <div>
<h1>Портал</h1> <h1>Портал</h1>
@ -79,4 +79,4 @@ function HelpPortal() {
); );
} }
export default HelpPortal; export default HelpMain;

View File

@ -1,24 +1,37 @@
import { IconOSS, IconPredecessor } from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpConceptOSS() { function HelpConceptOSS() {
return ( return (
<div> <div className='text-justify'>
<h1>Операционная схема синтеза</h1> <h1>Операционная схема синтеза</h1>
<p> <p>
Работа со сложными предметными областями требует многократного{' '} Работа со сложными предметными областями требует многократного{' '}
<LinkTopic text='синтеза' topic={HelpTopic.CC_SYNTHESIS} /> для построения целевых понятий. Последовательность <LinkTopic text='синтеза' topic={HelpTopic.CC_SYNTHESIS} /> для построения целевых понятий. Последовательность
синтезов концептуальных схем задается с помощью <b>Операционной схемы синтеза (ОСС)</b> в форме Графа синтеза. синтезов задается с помощью{' '}
<span className='text-nowrap'>
<IconOSS className='inline-icon' /> <b>Операционной схемы синтеза (ОСС)</b>
</span>{' '}
и отображается в форме <LinkTopic text='Графа синтеза' topic={HelpTopic.UI_OSS_GRAPH} />.
</p> </p>
<p> <p>
Отдельные операции в рамках ОСС задаются <b>таблицами отождествлений</b> понятий из синтезируемых схем. Таким Отдельные операции в рамках ОСС задаются <b>таблицами отождествлений</b> понятий из синтезируемых схем. Таким
образом <LinkTopic text='конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> в каждой КС разделяются на образом <LinkTopic text='конституенты' topic={HelpTopic.CC_CONSTITUENTA} /> в каждой КС разделяются на исходные
наследованные, отождествленные и дописанные. (дописанные), наследованные, отождествленные (удаляемые).
</p> </p>
<p> <p>
Портал поддерживает <b>сквозные изменения</b> в рамках ОСС. Изменения, внесенные в исходные концептуальные схемы Портал поддерживает <b>сквозные изменения</b> в рамках ОСС. Изменения, внесенные в исходные концептуальные схемы
автоматически проносятся через граф синтеза (путем обновления наследованных конституент). Формальные определения автоматически проносятся через граф синтеза (путем обновления наследованных конституент). Формальные определения
наследованных конституент можно редактировать только путем изменения исходных конституент. наследованных конституент можно редактировать только путем изменения{' '}
<span className='text-nowrap'>
<IconPredecessor className='inline-icon' /> исходных конституент.
</span>
</p>
<p>
<b>Ромбовидным синтезом</b> называется операция, где используются КС, имеющие общих предков. При таком синтезе
могут возникать дубликаты и неоднозначности в результате. Необходимо внимательно формировать таблицу
отождествлений, добавляя дублирующиеся понятия из синтезируемых схем.
</p> </p>
</div> </div>
); );

View File

@ -40,8 +40,8 @@ function HelpConceptSynthesis() {
<LinkTopic text='разделе Операции' topic={HelpTopic.RSL_OPERATIONS} /> <LinkTopic text='разделе Операции' topic={HelpTopic.RSL_OPERATIONS} />
</p> </p>
<p> <p>
Для управления совокупностью синтезов используются <b>операционные схемы синтеза</b>. В данный момент этот Для управления совокупностью синтезов используются{' '}
функционал еще не реализован в Портале. <LinkTopic text='операционные схемы синтеза' topic={HelpTopic.CC_OSS} />.
</p> </p>
</div> </div>
); );

View File

@ -5,18 +5,28 @@ import {
IconFolderEmpty, IconFolderEmpty,
IconFolderOpened, IconFolderOpened,
IconFolderTree, IconFolderTree,
IconOSS,
IconRSForm,
IconSearch, IconSearch,
IconShow, IconShow,
IconSortAsc, IconSortAsc,
IconSortDesc IconSortDesc
} from '@/components/Icons'; } from '@/components/Icons';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
function HelpLibrary() { function HelpLibrary() {
const { colors } = useConceptOptions();
return ( return (
<div> <div>
<h1>Библиотека схем</h1> <h1>Библиотека схем</h1>
<p>В библиотеке собраны концептуальные схемы, эксплицированные в родоструктурном аппарате</p> <p>
В библиотеке собраны <IconRSForm size='1rem' className='inline-icon' /> системы определений (КС) <br />и
<IconOSS size='1rem' className='inline-icon' /> операционные схемы синтеза (ОСС).
</p>
<li>
<span style={{ color: colors.fgGreen }}>зеленым текстом</span> выделены ОСС
</li>
<li>клик по строке - переход к редактированию схемы</li> <li>клик по строке - переход к редактированию схемы</li>
<li>Ctrl + клик по строке откроет схему в новой вкладке</li> <li>Ctrl + клик по строке откроет схему в новой вкладке</li>
<li>Фильтры атрибутов три позиции: да/нет/не применять</li> <li>Фильтры атрибутов три позиции: да/нет/не применять</li>

View File

@ -2,6 +2,7 @@ import {
IconAnimation, IconAnimation,
IconAnimationOff, IconAnimationOff,
IconConnect, IconConnect,
IconConsolidation,
IconDestroy, IconDestroy,
IconEdit2, IconEdit2,
IconExecute, IconExecute,
@ -23,7 +24,7 @@ import { HelpTopic } from '@/models/miscellaneous';
function HelpOssGraph() { function HelpOssGraph() {
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<h1>Граф синтеза</h1> <h1 className='sm:pr-[6rem]'>Граф синтеза</h1>
<div className='flex flex-col sm:flex-row'> <div className='flex flex-col sm:flex-row'>
<div className='w-full sm:w-[14rem]'> <div className='w-full sm:w-[14rem]'>
<h1>Настройка графа</h1> <h1>Настройка графа</h1>
@ -50,7 +51,10 @@ function HelpOssGraph() {
<li>Клик на операцию выделение</li> <li>Клик на операцию выделение</li>
<li>Esc сбросить выделение</li> <li>Esc сбросить выделение</li>
<li> <li>
<IconEdit2 className='inline-icon' /> Двойной клик редактирование Двойной клик переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
</li>
<li>
<IconEdit2 className='inline-icon' /> Редактирование операции
</li> </li>
<li> <li>
<IconNewItem className='inline-icon icon-green' /> Новая операция <IconNewItem className='inline-icon icon-green' /> Новая операция
@ -73,7 +77,7 @@ function HelpOssGraph() {
<IconSave className='inline-icon' /> Сохранить положения <IconSave className='inline-icon' /> Сохранить положения
</li> </li>
<li> <li>
<IconImage className='inline-icon' /> Сохранить в формат SVG <IconImage className='inline-icon' /> Сохранить в SVG
</li> </li>
</div> </div>
@ -82,9 +86,13 @@ function HelpOssGraph() {
<div className='dense w-[21rem]'> <div className='dense w-[21rem]'>
<h1>Контекстное меню</h1> <h1>Контекстное меню</h1>
<li> <li>
<IconRSForm className='inline-icon icon-green' /> Переход к связанной{' '} <IconRSForm className='inline-icon icon-green' /> Статус связанной{' '}
<LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} /> <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
</li> </li>
<li>
<IconConsolidation className='inline-icon' />{' '}
<LinkTopic text='Ромбовидный синтез' topic={HelpTopic.CC_OSS} />
</li>
<li> <li>
<IconNewRSForm className='inline-icon icon-green' /> Создать пустую КС для загрузки <IconNewRSForm className='inline-icon icon-green' /> Создать пустую КС для загрузки
</li> </li>

View File

@ -13,7 +13,7 @@ import {
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpRSFormCard() { function HelpRSCard() {
return ( return (
<div className='dense'> <div className='dense'>
<h1>Карточка схемы</h1> <h1>Карточка схемы</h1>
@ -64,4 +64,4 @@ function HelpRSFormCard() {
); );
} }
export default HelpRSFormCard; export default HelpRSCard;

View File

@ -22,7 +22,7 @@ import LinkTopic from '@/components/ui/LinkTopic';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpCstEditor() { function HelpRSEditor() {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
return ( return (
<div className='dense'> <div className='dense'>
@ -110,4 +110,4 @@ function HelpCstEditor() {
); );
} }
export default HelpCstEditor; export default HelpRSEditor;

View File

@ -22,7 +22,7 @@ import LinkTopic from '@/components/ui/LinkTopic';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpTermGraph() { function HelpRSGraphTerm() {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
@ -117,4 +117,4 @@ function HelpTermGraph() {
); );
} }
export default HelpTermGraph; export default HelpRSGraphTerm;

View File

@ -14,7 +14,7 @@ import Divider from '@/components/ui/Divider';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpRSFormItems() { function HelpRSList() {
return ( return (
<div className='dense'> <div className='dense'>
<h1>Список конституент</h1> <h1>Список конституент</h1>
@ -63,4 +63,4 @@ function HelpRSFormItems() {
); );
} }
export default HelpRSFormItems; export default HelpRSList;

View File

@ -17,7 +17,7 @@ import Divider from '@/components/ui/Divider';
import LinkTopic from '@/components/ui/LinkTopic'; import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
function HelpRSFormMenu() { function HelpRSMenu() {
return ( return (
<div> <div>
<h1>Редактирование схемы</h1> <h1>Редактирование схемы</h1>
@ -100,4 +100,4 @@ function HelpRSFormMenu() {
); );
} }
export default HelpRSFormMenu; export default HelpRSMenu;

View File

@ -21,11 +21,12 @@ function InputNode(node: OssNodeInternal) {
<> <>
<Handle type='source' position={Position.Bottom} /> <Handle type='source' position={Position.Bottom} />
<Overlay position='top-[-0.2rem] right-[-0.2rem]'> <Overlay position='top-0 right-0' className='flex'>
<MiniButton <MiniButton
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover
title='Связанная КС' noPadding
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
hideTitle={!controller.showTooltip} hideTitle={!controller.showTooltip}
onClick={() => { onClick={() => {
handleOpenSchema(); handleOpenSchema();

View File

@ -105,7 +105,7 @@ function NodeContextMenu({
<Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}> <Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}>
<DropdownButton <DropdownButton
text='Редактировать' text='Редактировать'
titleHtml={prepareTooltip('Редактировать операцию', 'Двойной клик')} title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleEditOperation} onClick={handleEditOperation}
@ -114,7 +114,7 @@ function NodeContextMenu({
{operation.result ? ( {operation.result ? (
<DropdownButton <DropdownButton
text='Открыть схему' text='Открыть схему'
title='Открыть привязанную КС' titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
icon={<IconRSForm size='1rem' className='icon-green' />} icon={<IconRSForm size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleOpenSchema} onClick={handleOpenSchema}

View File

@ -1,6 +1,6 @@
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { IconRSForm } from '@/components/Icons'; import { IconConsolidation, IconRSForm } from '@/components/Icons';
import TooltipOperation from '@/components/info/TooltipOperation'; import TooltipOperation from '@/components/info/TooltipOperation';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -22,15 +22,31 @@ function OperationNode(node: OssNodeInternal) {
<> <>
<Handle type='source' position={Position.Bottom} /> <Handle type='source' position={Position.Bottom} />
<Overlay position='top-[-0.2rem] right-[-0.2rem]'> <Overlay position='top-0 right-0' className='flex flex-col gap-1'>
<MiniButton <MiniButton
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={
<IconRSForm
className={hasFile ? 'clr-text-green' : 'clr-text-red'}
size={node.data.operation.is_consolidation ? '0.6rem' : '0.75rem'}
/>
}
noHover noHover
title='Связанная КС' noPadding
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
hideTitle={!controller.showTooltip} hideTitle={!controller.showTooltip}
onClick={handleOpenSchema} onClick={handleOpenSchema}
disabled={!hasFile} disabled={!hasFile}
/> />
{node.data.operation.is_consolidation ? (
<MiniButton
icon={<IconConsolidation className='clr-text-primary' size='0.6rem' />}
disabled
noPadding
noHover
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
hideTitle={!controller.showTooltip}
/>
) : null}
</Overlay> </Overlay>
{!node.data.operation.is_owned ? ( {!node.data.operation.is_owned ? (

View File

@ -299,9 +299,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
(event: CProps.EventMouse, node: OssNode) => { (event: CProps.EventMouse, node: OssNode) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (node.data.operation.result) {
controller.openOperationSchema(Number(node.id));
} else {
handleEditOperation(Number(node.id)); handleEditOperation(Number(node.id));
}
}, },
[handleEditOperation] [handleEditOperation, controller.openOperationSchema]
); );
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {

View File

@ -124,7 +124,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
onClick={editMenu.toggle} onClick={editMenu.toggle}
/> />
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
<div>операции над ОСС</div> <DropdownButton
text='см. Граф синтеза'
titleHtml='Редактирование доступно <br/>через Граф синтеза'
disabled
/>
</Dropdown> </Dropdown>
</div> </div>
) : null} ) : null}

View File

@ -8,6 +8,7 @@ import { urls } from '@/app/urls';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
@ -30,7 +31,7 @@ import {
} from '@/models/oss'; } from '@/models/oss';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { information } from '@/utils/labels'; import { errors, information } from '@/utils/labels';
export interface ICreateOperationPrompt { export interface ICreateOperationPrompt {
x: number; x: number;
@ -95,6 +96,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const { adminMode } = useConceptOptions(); const { adminMode } = useConceptOptions();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
const model = useOSS(); const model = useOSS();
const library = useLibrary();
const isMutable = useMemo( const isMutable = useMemo(
() => accessLevel > UserLevel.READER && !model.schema?.read_only, () => accessLevel > UserLevel.READER && !model.schema?.read_only,
@ -307,12 +309,20 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const createInput = useCallback( const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => { (target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
}
if (library.items.find(item => item.alias === operation.alias && item.location === model.schema!.location)) {
toast.error(errors.inputAlreadyExists);
return;
}
model.createInput({ target: target, positions: positions }, new_schema => { model.createInput({ target: target, positions: positions }, new_schema => {
toast.success(information.newLibraryItem); toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id)); router.push(urls.schema(new_schema.id));
}); });
}, },
[model, router] [model, library.items, router]
); );
const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => { const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => {

View File

@ -89,8 +89,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
/> />
{accessLevel >= UserLevel.OWNER ? ( {accessLevel >= UserLevel.OWNER ? (
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'> <Overlay position='top-[-0.5rem] left-[5.5rem]' className='cc-icons'>
<div className='flex items-start'>
<MiniButton <MiniButton
title='Изменить редакторов' title='Изменить редакторов'
noHover noHover
@ -98,7 +97,6 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />} icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
disabled={isModified || controller.isProcessing} disabled={isModified || controller.isProcessing}
/> />
</div>
</Overlay> </Overlay>
) : null} ) : null}
<LabeledValue <LabeledValue

View File

@ -30,7 +30,7 @@ import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useGlobalOss } from '@/context/GlobalOssContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
@ -50,7 +50,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const model = useRSForm(); const model = useRSForm();
const library = useLibrary(); const oss = useGlobalOss();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
@ -185,11 +185,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
) : null} ) : null}
{library.globalOSS ? ( {oss.schema ? (
<DropdownButton <DropdownButton
text='Перейти к ОСС' text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />} icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH))} onClick={() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH))}
/> />
) : null} ) : null}
<DropdownButton <DropdownButton

View File

@ -953,7 +953,8 @@ export const errors = {
passwordsMismatch: 'Пароли не совпадают', passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения', imageFailed: 'Ошибка при создании изображения',
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении', reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',
substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении' substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении',
inputAlreadyExists: 'Концептуальная схема с таким именем уже существует'
}; };
/** /**

View File

@ -2,7 +2,7 @@
* Module: Utility functions. * Module: Utility functions.
*/ */
import { AxiosHeaderValue, AxiosResponse } from 'axios'; import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
import { prompts } from './labels'; import { prompts } from './labels';
@ -139,3 +139,22 @@ export function tripleToggleColor(value: boolean | undefined): string {
} }
return value ? 'clr-text-green' : 'clr-text-red'; return value ? 'clr-text-green' : 'clr-text-red';
} }
/**
* Extract error message from error object.
*/
export function extractErrorMessage(error: Error | AxiosError): string {
if (axios.isAxiosError(error)) {
if (error.response && error.response.status === 400) {
const data = error.response.data as Record<string, unknown>;
const keys = Object.keys(data);
if (keys.length === 1) {
const value = data[keys[0]];
if (typeof value === 'string') {
return `${keys[0]}: ${value}`;
}
}
}
}
return error.message;
}