Compare commits
37 Commits
dbceac0a6d
...
e2ab676ec2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e2ab676ec2 | ||
![]() |
822533f293 | ||
![]() |
5cba240d71 | ||
![]() |
1f5cad5073 | ||
![]() |
11d0ec6fac | ||
![]() |
0e32c70610 | ||
![]() |
687aa55dcb | ||
![]() |
7f0c0fd70e | ||
![]() |
bac6650301 | ||
![]() |
7496389c31 | ||
![]() |
34893818fa | ||
![]() |
290081ed35 | ||
![]() |
d786154374 | ||
![]() |
f820d84cb0 | ||
![]() |
12028471bd | ||
![]() |
3d77317347 | ||
![]() |
d40e1ea256 | ||
![]() |
a3241ecff7 | ||
![]() |
5f049a929d | ||
![]() |
9f5fe24ad6 | ||
![]() |
6b68375b01 | ||
![]() |
3d81a7dc28 | ||
![]() |
58040f593f | ||
![]() |
070ab18231 | ||
![]() |
5dafe0a3e7 | ||
![]() |
c9c1d985b6 | ||
![]() |
a78a594509 | ||
![]() |
13914a04f9 | ||
![]() |
2ae9576384 | ||
![]() |
c07bdfbb3a | ||
![]() |
6783300339 | ||
![]() |
da035478f6 | ||
![]() |
9df4c6799d | ||
![]() |
5f767c943d | ||
![]() |
6cb0fd71ba | ||
![]() |
09071a6e8f | ||
![]() |
40314dbb63 |
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
@ -6,11 +6,11 @@
|
||||||
"fractalbrew.backticks",
|
"fractalbrew.backticks",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
"streetsidesoftware.code-spell-checker-russian",
|
"streetsidesoftware.code-spell-checker-russian",
|
||||||
"kamikillerto.vscode-colorize",
|
|
||||||
"batisteo.vscode-django",
|
"batisteo.vscode-django",
|
||||||
"ms-azuretools.vscode-docker",
|
"ms-azuretools.vscode-docker",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"seyyedkhandon.firacode",
|
"seyyedkhandon.firacode",
|
||||||
|
"nize.oklch-preview",
|
||||||
"ms-python.isort",
|
"ms-python.isort",
|
||||||
"ms-vscode.powershell",
|
"ms-vscode.powershell",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
|
|
|
@ -85,7 +85,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
<summary>VS Code plugins</summary>
|
<summary>VS Code plugins</summary>
|
||||||
<pre>
|
<pre>
|
||||||
- ESLint
|
- ESLint
|
||||||
- Colorize
|
- Oklch Color Preview
|
||||||
- Tailwind CSS IntelliSense
|
- Tailwind CSS IntelliSense
|
||||||
- Code Spell Checker (eng + rus)
|
- Code Spell Checker (eng + rus)
|
||||||
- Backticks
|
- Backticks
|
||||||
|
|
1
TODO.txt
1
TODO.txt
|
@ -15,6 +15,7 @@ User profile:
|
||||||
- Profile pictures
|
- Profile pictures
|
||||||
- Custom LibraryItem lists
|
- Custom LibraryItem lists
|
||||||
- Custom user filters and sharing filters
|
- Custom user filters and sharing filters
|
||||||
|
- Personal prompt templates
|
||||||
|
|
||||||
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
|
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
|
||||||
- OSS clone and versioning
|
- OSS clone and versioning
|
||||||
|
|
|
@ -92,26 +92,26 @@ class OperationSchema:
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_layout(self, data: dict) -> None:
|
def update_layout(self, data: dict) -> None:
|
||||||
''' Update positions. '''
|
''' Update graphical layout. '''
|
||||||
layout = self.layout()
|
layout = self.layout()
|
||||||
layout.data = data
|
layout.data = data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
||||||
def create_operation(self, **kwargs) -> Operation:
|
def create_operation(self, **kwargs) -> Operation:
|
||||||
''' Insert new operation. '''
|
''' Create Operation. '''
|
||||||
result = Operation.objects.create(oss=self.model, **kwargs)
|
result = Operation.objects.create(oss=self.model, **kwargs)
|
||||||
self.cache.insert_operation(result)
|
self.cache.insert_operation(result)
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def create_block(self, **kwargs) -> Block:
|
def create_block(self, **kwargs) -> Block:
|
||||||
''' Insert new block. '''
|
''' Create Block. '''
|
||||||
result = Block.objects.create(oss=self.model, **kwargs)
|
result = Block.objects.create(oss=self.model, **kwargs)
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def delete_operation(self, target: int, keep_constituents: bool = False):
|
def delete_operation(self, target: int, keep_constituents: bool = False):
|
||||||
''' Delete operation. '''
|
''' Delete Operation. '''
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
schema = self.cache.get_schema(operation)
|
schema = self.cache.get_schema(operation)
|
||||||
|
@ -139,6 +139,20 @@ class OperationSchema:
|
||||||
operation.delete()
|
operation.delete()
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
def delete_block(self, target: Block):
|
||||||
|
''' Delete Block. '''
|
||||||
|
new_parent = target.parent
|
||||||
|
if new_parent is not None:
|
||||||
|
for block in Block.objects.filter(parent=target):
|
||||||
|
if block != new_parent:
|
||||||
|
block.parent = new_parent
|
||||||
|
block.save(update_fields=['parent'])
|
||||||
|
for operation in Operation.objects.filter(parent=target):
|
||||||
|
operation.parent = new_parent
|
||||||
|
operation.save(update_fields=['parent'])
|
||||||
|
target.delete()
|
||||||
|
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:
|
||||||
''' Set input schema for operation. '''
|
''' Set input schema for operation. '''
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
|
@ -165,7 +179,7 @@ class OperationSchema:
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
|
|
||||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||||
''' Set arguments to operation. '''
|
''' Set arguments of target Operation. '''
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
processed: list[Operation] = []
|
processed: list[Operation] = []
|
||||||
|
@ -198,7 +212,7 @@ class OperationSchema:
|
||||||
self.save(update_fields=['time_update'])
|
self.save(update_fields=['time_update'])
|
||||||
|
|
||||||
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
||||||
''' Clear all arguments for operation. '''
|
''' Clear all arguments for target Operation. '''
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
schema = self.cache.get_schema(operation)
|
schema = self.cache.get_schema(operation)
|
||||||
|
@ -237,7 +251,7 @@ class OperationSchema:
|
||||||
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:
|
||||||
''' Create input RSForm. '''
|
''' Create input RSForm for given Operation. '''
|
||||||
schema = RSForm.create(
|
schema = RSForm.create(
|
||||||
owner=self.model.owner,
|
owner=self.model.owner,
|
||||||
alias=operation.alias,
|
alias=operation.alias,
|
||||||
|
@ -254,7 +268,7 @@ class OperationSchema:
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def execute_operation(self, operation: Operation) -> bool:
|
def execute_operation(self, operation: Operation) -> bool:
|
||||||
''' Execute target operation. '''
|
''' Execute target Operation. '''
|
||||||
schemas = [
|
schemas = [
|
||||||
arg.argument.result
|
arg.argument.result
|
||||||
for arg in operation.getQ_arguments().order_by('order')
|
for arg in operation.getQ_arguments().order_by('order')
|
||||||
|
@ -301,7 +315,7 @@ class OperationSchema:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]):
|
def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]):
|
||||||
''' Move list of constituents to specific schema inheritor. '''
|
''' Move list of Constituents to destination Schema inheritor. '''
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
self.cache.insert_schema(destination)
|
self.cache.insert_schema(destination)
|
||||||
|
@ -315,7 +329,7 @@ class OperationSchema:
|
||||||
Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete()
|
Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete()
|
||||||
|
|
||||||
def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]:
|
def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]:
|
||||||
''' Move list of constituents to specific schema upstream. '''
|
''' Move list of Constituents upstream to destination Schema. '''
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
self.cache.insert_schema(source)
|
self.cache.insert_schema(source)
|
||||||
self.cache.insert_schema(destination)
|
self.cache.insert_schema(destination)
|
||||||
|
@ -345,7 +359,7 @@ class OperationSchema:
|
||||||
cst_list: list[Constituenta],
|
cst_list: list[Constituenta],
|
||||||
exclude: Optional[list[int]] = None
|
exclude: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Trigger cascade resolutions when new constituent is created. '''
|
''' Trigger cascade resolutions when new Constituenta is created. '''
|
||||||
self.cache.insert_schema(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()
|
||||||
|
@ -361,13 +375,13 @@ class OperationSchema:
|
||||||
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
||||||
|
|
||||||
def after_change_cst_type(self, source: RSForm, target: Constituenta) -> 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_schema(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(operation.pk, target.pk, cast(CstType, target.cst_type))
|
self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type))
|
||||||
|
|
||||||
def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> 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_schema(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)
|
||||||
|
@ -385,13 +399,13 @@ class OperationSchema:
|
||||||
)
|
)
|
||||||
|
|
||||||
def before_delete_cst(self, source: RSForm, target: list[Constituenta]) -> 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_schema(source)
|
self.cache.insert_schema(source)
|
||||||
operation = self.cache.get_operation(source.model.pk)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
self._cascade_delete_inherited(operation.pk, target)
|
self._cascade_delete_inherited(operation.pk, target)
|
||||||
|
|
||||||
def before_substitute(self, source: RSForm, substitutions: CstSubstitution) -> 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_schema(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)
|
||||||
|
|
|
@ -17,7 +17,7 @@ class PropagationFacade:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when new constituent is created. '''
|
''' Trigger cascade resolutions when new constituenta is created. '''
|
||||||
hosts = _get_oss_hosts(source.model)
|
hosts = _get_oss_hosts(source.model)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host.pk not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
|
|
|
@ -3,20 +3,23 @@
|
||||||
from .basics import LayoutSerializer, SubstitutionExSerializer
|
from .basics import LayoutSerializer, SubstitutionExSerializer
|
||||||
from .data_access import (
|
from .data_access import (
|
||||||
ArgumentSerializer,
|
ArgumentSerializer,
|
||||||
BlockCreateSerializer,
|
|
||||||
BlockSerializer,
|
BlockSerializer,
|
||||||
OperationCreateSerializer,
|
CreateBlockSerializer,
|
||||||
OperationDeleteSerializer,
|
CreateOperationSerializer,
|
||||||
|
DeleteBlockSerializer,
|
||||||
|
DeleteOperationSerializer,
|
||||||
|
MoveItemsSerializer,
|
||||||
OperationSchemaSerializer,
|
OperationSchemaSerializer,
|
||||||
OperationSerializer,
|
OperationSerializer,
|
||||||
OperationTargetSerializer,
|
|
||||||
OperationUpdateSerializer,
|
|
||||||
RelocateConstituentsSerializer,
|
RelocateConstituentsSerializer,
|
||||||
SetOperationInputSerializer
|
SetOperationInputSerializer,
|
||||||
|
TargetOperationSerializer,
|
||||||
|
UpdateBlockSerializer,
|
||||||
|
UpdateOperationSerializer
|
||||||
)
|
)
|
||||||
from .responses import (
|
from .responses import (
|
||||||
|
BlockCreatedResponse,
|
||||||
ConstituentaReferenceResponse,
|
ConstituentaReferenceResponse,
|
||||||
NewBlockResponse,
|
OperationCreatedResponse,
|
||||||
NewOperationResponse,
|
SchemaCreatedResponse
|
||||||
NewSchemaResponse
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
''' Serializers for persistent data manipulation. '''
|
''' Serializers for persistent data manipulation. '''
|
||||||
|
from collections import deque
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
@ -41,7 +42,7 @@ class ArgumentSerializer(serializers.ModelSerializer):
|
||||||
fields = ('operation', 'argument')
|
fields = ('operation', 'argument')
|
||||||
|
|
||||||
|
|
||||||
class BlockCreateSerializer(serializers.Serializer):
|
class CreateBlockSerializer(serializers.Serializer):
|
||||||
''' Serializer: Block creation. '''
|
''' Serializer: Block creation. '''
|
||||||
class BlockCreateData(serializers.ModelSerializer):
|
class BlockCreateData(serializers.ModelSerializer):
|
||||||
''' Serializer: Block creation data. '''
|
''' Serializer: Block creation data. '''
|
||||||
|
@ -52,7 +53,6 @@ class BlockCreateSerializer(serializers.Serializer):
|
||||||
fields = 'title', 'description', 'parent'
|
fields = 'title', 'description', 'parent'
|
||||||
|
|
||||||
layout = LayoutSerializer()
|
layout = LayoutSerializer()
|
||||||
|
|
||||||
item_data = BlockCreateData()
|
item_data = BlockCreateData()
|
||||||
width = serializers.FloatField()
|
width = serializers.FloatField()
|
||||||
height = serializers.FloatField()
|
height = serializers.FloatField()
|
||||||
|
@ -63,9 +63,10 @@ class BlockCreateSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
if 'parent' in attrs['item_data'] and \
|
parent = attrs['item_data'].get('parent')
|
||||||
attrs['item_data']['parent'] is not None and \
|
children_blocks = attrs.get('children_blocks', [])
|
||||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
|
||||||
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
@ -76,17 +77,114 @@ class BlockCreateSerializer(serializers.Serializer):
|
||||||
'children_operations': msg.childNotInOSS()
|
'children_operations': msg.childNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
for block in attrs['children_blocks']:
|
for block in children_blocks:
|
||||||
if block.oss_id != oss.pk:
|
if block.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'children_blocks': msg.childNotInOSS()
|
'children_blocks': msg.childNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
descendant_ids = _collect_descendants(children_blocks)
|
||||||
|
if parent.pk in descendant_ids:
|
||||||
|
raise serializers.ValidationError({'parent': msg.blockCyclicHierarchy()})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class OperationCreateSerializer(serializers.Serializer):
|
class UpdateBlockSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Block update. '''
|
||||||
|
class UpdateBlockData(serializers.ModelSerializer):
|
||||||
|
''' Serializer: Block update data. '''
|
||||||
|
class Meta:
|
||||||
|
''' serializer metadata. '''
|
||||||
|
model = Block
|
||||||
|
fields = 'title', 'description', 'parent'
|
||||||
|
|
||||||
|
layout = LayoutSerializer(required=False)
|
||||||
|
target = PKField(many=False, queryset=Block.objects.all())
|
||||||
|
item_data = UpdateBlockData()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
|
block = cast(Block, attrs['target'])
|
||||||
|
if block.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'target': msg.blockNotInOSS()
|
||||||
|
})
|
||||||
|
|
||||||
|
parent = attrs['item_data'].get('parent')
|
||||||
|
if parent is not None:
|
||||||
|
if parent.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'parent': msg.parentNotInOSS()
|
||||||
|
})
|
||||||
|
if parent == attrs['target']:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'parent': msg.blockCyclicHierarchy()
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteBlockSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Delete block. '''
|
||||||
|
layout = LayoutSerializer()
|
||||||
|
target = PKField(many=False, queryset=Block.objects.all().only('oss_id'))
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
|
block = cast(Block, attrs['target'])
|
||||||
|
if block.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'target': msg.blockNotInOSS()
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class MoveItemsSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Move items to another parent. '''
|
||||||
|
layout = LayoutSerializer()
|
||||||
|
operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'parent'))
|
||||||
|
blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id', 'parent'))
|
||||||
|
destination = PKField(many=False, queryset=Block.objects.all().only('oss_id'), allow_null=True)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
|
parent_block = cast(Block, attrs['destination'])
|
||||||
|
moved_blocks = attrs.get('blocks', [])
|
||||||
|
moved_operations = attrs.get('operations', [])
|
||||||
|
|
||||||
|
if parent_block is not None and parent_block.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.blockNotInOSS()
|
||||||
|
})
|
||||||
|
for operation in moved_operations:
|
||||||
|
if operation.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'operations': msg.operationNotInOSS()
|
||||||
|
})
|
||||||
|
for block in moved_blocks:
|
||||||
|
if parent_block is not None and block.pk == parent_block.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.blockCyclicHierarchy()
|
||||||
|
})
|
||||||
|
if block.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'blocks': msg.blockNotInOSS()
|
||||||
|
})
|
||||||
|
|
||||||
|
if parent_block:
|
||||||
|
ancestor_ids = _collect_ancestors(parent_block)
|
||||||
|
moved_block_ids = {b.pk for b in moved_blocks}
|
||||||
|
if moved_block_ids & ancestor_ids:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.blockCyclicHierarchy()
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class CreateOperationSerializer(serializers.Serializer):
|
||||||
''' Serializer: Operation creation. '''
|
''' Serializer: Operation creation. '''
|
||||||
class OperationCreateData(serializers.ModelSerializer):
|
class CreateOperationData(serializers.ModelSerializer):
|
||||||
''' Serializer: Operation creation data. '''
|
''' Serializer: Operation creation data. '''
|
||||||
alias = serializers.CharField()
|
alias = serializers.CharField()
|
||||||
operation_type = serializers.ChoiceField(OperationType.choices)
|
operation_type = serializers.ChoiceField(OperationType.choices)
|
||||||
|
@ -99,8 +197,7 @@ class OperationCreateSerializer(serializers.Serializer):
|
||||||
'description', 'result', 'parent'
|
'description', 'result', 'parent'
|
||||||
|
|
||||||
layout = LayoutSerializer()
|
layout = LayoutSerializer()
|
||||||
|
item_data = CreateOperationData()
|
||||||
item_data = OperationCreateData()
|
|
||||||
position_x = serializers.FloatField()
|
position_x = serializers.FloatField()
|
||||||
position_y = serializers.FloatField()
|
position_y = serializers.FloatField()
|
||||||
create_schema = serializers.BooleanField(default=False, required=False)
|
create_schema = serializers.BooleanField(default=False, required=False)
|
||||||
|
@ -108,9 +205,8 @@ class OperationCreateSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
if 'parent' in attrs['item_data'] and \
|
parent = attrs['item_data'].get('parent')
|
||||||
attrs['item_data']['parent'] is not None and \
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
attrs['item_data']['parent'].oss_id != oss.pk:
|
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
@ -120,23 +216,23 @@ class OperationCreateSerializer(serializers.Serializer):
|
||||||
for operation in attrs['arguments']:
|
for operation in attrs['arguments']:
|
||||||
if operation.oss_id != oss.pk:
|
if operation.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'arguments': msg.operationNotInOSS(oss.title)
|
'arguments': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class OperationUpdateSerializer(serializers.Serializer):
|
class UpdateOperationSerializer(serializers.Serializer):
|
||||||
''' Serializer: Operation update. '''
|
''' Serializer: Operation update. '''
|
||||||
class OperationUpdateData(serializers.ModelSerializer):
|
class UpdateOperationData(serializers.ModelSerializer):
|
||||||
''' Serializer: Operation update data. '''
|
''' Serializer: Operation update data. '''
|
||||||
class Meta:
|
class Meta:
|
||||||
''' serializer metadata. '''
|
''' serializer metadata. '''
|
||||||
model = Operation
|
model = Operation
|
||||||
fields = 'alias', 'title', 'description'
|
fields = 'alias', 'title', 'description', 'parent'
|
||||||
|
|
||||||
layout = LayoutSerializer()
|
layout = LayoutSerializer(required=False)
|
||||||
target = PKField(many=False, queryset=Operation.objects.all())
|
target = PKField(many=False, queryset=Operation.objects.all())
|
||||||
item_data = OperationUpdateData()
|
item_data = UpdateOperationData()
|
||||||
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False)
|
||||||
substitutions = serializers.ListField(
|
substitutions = serializers.ListField(
|
||||||
child=SubstitutionSerializerBase(),
|
child=SubstitutionSerializerBase(),
|
||||||
|
@ -145,7 +241,14 @@ class OperationUpdateSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
if 'parent' in attrs['item_data'] and attrs['item_data']['parent'].oss_id != oss.pk:
|
parent = attrs['item_data'].get('parent')
|
||||||
|
target = cast(Block, attrs['target'])
|
||||||
|
if target.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'target': msg.operationNotInOSS()
|
||||||
|
})
|
||||||
|
|
||||||
|
if parent is not None and parent.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'parent': msg.parentNotInOSS()
|
'parent': msg.parentNotInOSS()
|
||||||
})
|
})
|
||||||
|
@ -160,7 +263,7 @@ class OperationUpdateSerializer(serializers.Serializer):
|
||||||
for operation in attrs['arguments']:
|
for operation in attrs['arguments']:
|
||||||
if operation.oss_id != oss.pk:
|
if operation.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'arguments': msg.operationNotInOSS(oss.title)
|
'arguments': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
|
|
||||||
if 'substitutions' not in attrs:
|
if 'substitutions' not in attrs:
|
||||||
|
@ -192,22 +295,7 @@ class OperationUpdateSerializer(serializers.Serializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class OperationTargetSerializer(serializers.Serializer):
|
class DeleteOperationSerializer(serializers.Serializer):
|
||||||
''' Serializer: Target single operation. '''
|
|
||||||
layout = LayoutSerializer()
|
|
||||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
|
||||||
operation = cast(Operation, attrs['target'])
|
|
||||||
if oss and operation.oss_id != oss.pk:
|
|
||||||
raise serializers.ValidationError({
|
|
||||||
'target': msg.operationNotInOSS(oss.title)
|
|
||||||
})
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class OperationDeleteSerializer(serializers.Serializer):
|
|
||||||
''' Serializer: Delete operation. '''
|
''' Serializer: Delete operation. '''
|
||||||
layout = LayoutSerializer()
|
layout = LayoutSerializer()
|
||||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
||||||
|
@ -217,9 +305,24 @@ class OperationDeleteSerializer(serializers.Serializer):
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
oss = cast(LibraryItem, self.context['oss'])
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
operation = cast(Operation, attrs['target'])
|
operation = cast(Operation, attrs['target'])
|
||||||
if oss and operation.oss_id != oss.pk:
|
if operation.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInOSS(oss.title)
|
'target': msg.operationNotInOSS()
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class TargetOperationSerializer(serializers.Serializer):
|
||||||
|
''' Serializer: Target single operation. '''
|
||||||
|
layout = LayoutSerializer()
|
||||||
|
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id'))
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
oss = cast(LibraryItem, self.context['oss'])
|
||||||
|
operation = cast(Operation, attrs['target'])
|
||||||
|
if operation.oss_id != oss.pk:
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -240,7 +343,7 @@ class SetOperationInputSerializer(serializers.Serializer):
|
||||||
operation = cast(Operation, attrs['target'])
|
operation = cast(Operation, attrs['target'])
|
||||||
if oss and operation.oss_id != oss.pk:
|
if oss and operation.oss_id != oss.pk:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'target': msg.operationNotInOSS(oss.title)
|
'target': msg.operationNotInOSS()
|
||||||
})
|
})
|
||||||
if operation.operation_type != OperationType.INPUT:
|
if operation.operation_type != OperationType.INPUT:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
|
@ -355,3 +458,29 @@ class RelocateConstituentsSerializer(serializers.Serializer):
|
||||||
})
|
})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
# ====== Internals =================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_descendants(start_blocks: list[Block]) -> set[int]:
|
||||||
|
""" Recursively collect all descendant block IDs from a list of blocks. """
|
||||||
|
visited = set()
|
||||||
|
queue = deque(start_blocks)
|
||||||
|
while queue:
|
||||||
|
block = queue.popleft()
|
||||||
|
if block.pk not in visited:
|
||||||
|
visited.add(block.pk)
|
||||||
|
queue.extend(block.as_child_block.all())
|
||||||
|
return visited
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_ancestors(block: Block) -> set[int]:
|
||||||
|
""" Recursively collect all ancestor block IDs of a block. """
|
||||||
|
ancestors = set()
|
||||||
|
current = block.parent
|
||||||
|
while current:
|
||||||
|
if current.pk in ancestors:
|
||||||
|
break # Prevent infinite loop in malformed data
|
||||||
|
ancestors.add(current.pk)
|
||||||
|
current = current.parent
|
||||||
|
return ancestors
|
||||||
|
|
|
@ -6,19 +6,19 @@ from apps.library.serializers import LibraryItemSerializer
|
||||||
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer
|
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer
|
||||||
|
|
||||||
|
|
||||||
class NewOperationResponse(serializers.Serializer):
|
class OperationCreatedResponse(serializers.Serializer):
|
||||||
''' Serializer: Create operation response. '''
|
''' Serializer: Create operation response. '''
|
||||||
new_operation = OperationSerializer()
|
new_operation = OperationSerializer()
|
||||||
oss = OperationSchemaSerializer()
|
oss = OperationSchemaSerializer()
|
||||||
|
|
||||||
|
|
||||||
class NewBlockResponse(serializers.Serializer):
|
class BlockCreatedResponse(serializers.Serializer):
|
||||||
''' Serializer: Create block response. '''
|
''' Serializer: Create block response. '''
|
||||||
new_block = BlockSerializer()
|
new_block = BlockSerializer()
|
||||||
oss = OperationSchemaSerializer()
|
oss = OperationSchemaSerializer()
|
||||||
|
|
||||||
|
|
||||||
class NewSchemaResponse(serializers.Serializer):
|
class SchemaCreatedResponse(serializers.Serializer):
|
||||||
''' Serializer: Create RSForm for input operation response. '''
|
''' Serializer: Create RSForm for input operation response. '''
|
||||||
new_schema = LibraryItemSerializer()
|
new_schema = LibraryItemSerializer()
|
||||||
oss = OperationSchemaSerializer()
|
oss = OperationSchemaSerializer()
|
||||||
|
|
|
@ -27,6 +27,10 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.block1 = self.owned.create_block(
|
self.block1 = self.owned.create_block(
|
||||||
title='1',
|
title='1',
|
||||||
)
|
)
|
||||||
|
self.block2 = self.owned.create_block(
|
||||||
|
title='2',
|
||||||
|
parent=self.block1
|
||||||
|
)
|
||||||
self.operation1 = self.owned.create_operation(
|
self.operation1 = self.owned.create_operation(
|
||||||
alias='1',
|
alias='1',
|
||||||
operation_type=OperationType.INPUT,
|
operation_type=OperationType.INPUT,
|
||||||
|
@ -35,15 +39,12 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.operation2 = self.owned.create_operation(
|
self.operation2 = self.owned.create_operation(
|
||||||
alias='2',
|
alias='2',
|
||||||
operation_type=OperationType.INPUT,
|
operation_type=OperationType.INPUT,
|
||||||
|
parent=self.block2,
|
||||||
)
|
)
|
||||||
self.operation3 = self.unowned.create_operation(
|
self.operation3 = self.unowned.create_operation(
|
||||||
alias='3',
|
alias='3',
|
||||||
operation_type=OperationType.INPUT
|
operation_type=OperationType.INPUT
|
||||||
)
|
)
|
||||||
self.block2 = self.owned.create_block(
|
|
||||||
title='2',
|
|
||||||
parent=self.block1
|
|
||||||
)
|
|
||||||
self.block3 = self.unowned.create_block(
|
self.block3 = self.unowned.create_block(
|
||||||
title='3',
|
title='3',
|
||||||
parent=self.block1
|
parent=self.block1
|
||||||
|
@ -165,3 +166,99 @@ class TestOssBlocks(EndpointTester):
|
||||||
self.block1.refresh_from_db()
|
self.block1.refresh_from_db()
|
||||||
self.assertEqual(self.operation1.parent.pk, new_block['id'])
|
self.assertEqual(self.operation1.parent.pk, new_block['id'])
|
||||||
self.assertEqual(self.block1.parent.pk, new_block['id'])
|
self.assertEqual(self.block1.parent.pk, new_block['id'])
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/create-block', method='post')
|
||||||
|
def test_create_block_cyclic(self):
|
||||||
|
self.populateData()
|
||||||
|
data = {
|
||||||
|
'item_data': {
|
||||||
|
'title': 'Test title',
|
||||||
|
'description': 'Тест кириллицы',
|
||||||
|
'parent': self.block2.pk
|
||||||
|
},
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'position_x': 1337,
|
||||||
|
'position_y': 1337,
|
||||||
|
'width': 0.42,
|
||||||
|
'height': 0.42,
|
||||||
|
'children_operations': [],
|
||||||
|
'children_blocks': [self.block1.pk]
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data, item=self.owned_id)
|
||||||
|
|
||||||
|
data['item_data']['parent'] = self.block1.pk
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['children_blocks'] = [self.block2.pk]
|
||||||
|
self.executeCreated(data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
|
||||||
|
def test_delete_block(self):
|
||||||
|
self.populateData()
|
||||||
|
self.executeNotFound(item=self.invalid_id)
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['target'] = self.operation1.pk
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['target'] = self.block3.pk
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['target'] = self.block2.pk
|
||||||
|
self.logout()
|
||||||
|
self.executeForbidden(data=data)
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
response = self.executeOK(data=data)
|
||||||
|
self.operation2.refresh_from_db()
|
||||||
|
self.assertEqual(len(response.data['blocks']), 1)
|
||||||
|
self.assertEqual(self.operation2.parent.pk, self.block1.pk)
|
||||||
|
|
||||||
|
data['target'] = self.block1.pk
|
||||||
|
response = self.executeOK(data=data)
|
||||||
|
self.operation1.refresh_from_db()
|
||||||
|
self.operation2.refresh_from_db()
|
||||||
|
self.assertEqual(len(response.data['blocks']), 0)
|
||||||
|
self.assertEqual(self.operation1.parent, None)
|
||||||
|
self.assertEqual(self.operation2.parent, None)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/update-block', method='patch')
|
||||||
|
def test_update_block(self):
|
||||||
|
self.populateData()
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'target': self.invalid_id,
|
||||||
|
'item_data': {
|
||||||
|
'title': 'Test title mod',
|
||||||
|
'description': 'Comment mod',
|
||||||
|
'parent': None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['target'] = self.block3.pk
|
||||||
|
self.toggle_admin(True)
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['target'] = self.block2.pk
|
||||||
|
self.logout()
|
||||||
|
self.executeForbidden(data=data)
|
||||||
|
|
||||||
|
self.login()
|
||||||
|
response = self.executeOK(data=data)
|
||||||
|
self.block2.refresh_from_db()
|
||||||
|
self.assertEqual(self.block2.title, data['item_data']['title'])
|
||||||
|
self.assertEqual(self.block2.description, data['item_data']['description'])
|
||||||
|
self.assertEqual(self.block2.parent, data['item_data']['parent'])
|
||||||
|
|
||||||
|
data['layout'] = self.layout_data
|
||||||
|
self.executeOK(data=data)
|
||||||
|
|
|
@ -226,9 +226,8 @@ class TestOssOperations(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||||
def test_delete_operation(self):
|
def test_delete_operation(self):
|
||||||
self.executeNotFound(item=self.invalid_id)
|
|
||||||
|
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
self.executeNotFound(item=self.invalid_id)
|
||||||
self.executeBadData(item=self.owned_id)
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
@ -371,7 +370,6 @@ class TestOssOperations(EndpointTester):
|
||||||
'title': 'Test title mod',
|
'title': 'Test title mod',
|
||||||
'description': 'Comment mod'
|
'description': 'Comment mod'
|
||||||
},
|
},
|
||||||
'layout': self.layout_data,
|
|
||||||
'arguments': [self.operation2.pk, self.operation1.pk],
|
'arguments': [self.operation2.pk, self.operation1.pk],
|
||||||
'substitutions': [
|
'substitutions': [
|
||||||
{
|
{
|
||||||
|
@ -403,6 +401,10 @@ class TestOssOperations(EndpointTester):
|
||||||
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
|
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
|
||||||
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
|
||||||
|
|
||||||
|
data['layout'] = self.layout_data
|
||||||
|
self.executeOK(data=data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
def test_update_operation_sync(self):
|
def test_update_operation_sync(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
|
@ -55,12 +55,13 @@ class TestOssViewset(EndpointTester):
|
||||||
alias='3',
|
alias='3',
|
||||||
operation_type=OperationType.SYNTHESIS
|
operation_type=OperationType.SYNTHESIS
|
||||||
)
|
)
|
||||||
layout = self.owned.layout()
|
self.layout_data = {'operations': [
|
||||||
layout.data = {'operations': [
|
|
||||||
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
{'id': self.operation1.pk, 'x': 0, 'y': 0},
|
||||||
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
{'id': self.operation2.pk, 'x': 0, 'y': 0},
|
||||||
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
{'id': self.operation3.pk, 'x': 0, 'y': 0},
|
||||||
], 'blocks': []}
|
], 'blocks': []}
|
||||||
|
layout = self.owned.layout()
|
||||||
|
layout.data = self.layout_data
|
||||||
layout.save()
|
layout.save()
|
||||||
|
|
||||||
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
|
self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2])
|
||||||
|
@ -167,6 +168,61 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['id'], self.ks1X2.pk)
|
self.assertEqual(response.data['id'], self.ks1X2.pk)
|
||||||
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
self.assertEqual(response.data['schema'], self.ks1.model.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/move-items', method='patch')
|
||||||
|
def test_move_items(self):
|
||||||
|
self.populateData()
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
|
block1 = self.owned.create_block(title='1')
|
||||||
|
block2 = self.owned.create_block(title='2')
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'blocks': [block2.pk],
|
||||||
|
'operations': [self.operation1.pk, self.operation2.pk],
|
||||||
|
'destination': block2.pk
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data['destination'] = block1.pk
|
||||||
|
self.executeOK(data=data)
|
||||||
|
self.operation1.refresh_from_db()
|
||||||
|
self.operation2.refresh_from_db()
|
||||||
|
block2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.operation1.parent.pk, block1.pk)
|
||||||
|
self.assertEqual(self.operation2.parent.pk, block1.pk)
|
||||||
|
self.assertEqual(block2.parent.pk, block1.pk)
|
||||||
|
|
||||||
|
data['destination'] = None
|
||||||
|
self.executeOK(data=data)
|
||||||
|
self.operation1.refresh_from_db()
|
||||||
|
self.operation2.refresh_from_db()
|
||||||
|
block2.refresh_from_db()
|
||||||
|
self.assertEqual(block2.parent, None)
|
||||||
|
self.assertEqual(self.operation1.parent, None)
|
||||||
|
self.assertEqual(self.operation2.parent, None)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/oss/{item}/move-items', method='patch')
|
||||||
|
def test_move_items_cyclic(self):
|
||||||
|
self.populateData()
|
||||||
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
||||||
|
block1 = self.owned.create_block(title='1')
|
||||||
|
block2 = self.owned.create_block(title='2', parent=block1)
|
||||||
|
block3 = self.owned.create_block(title='3', parent=block2)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'layout': self.layout_data,
|
||||||
|
'blocks': [block1.pk],
|
||||||
|
'operations': [],
|
||||||
|
'destination': block3.pk
|
||||||
|
}
|
||||||
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
def test_relocate_constituents(self):
|
def test_relocate_constituents(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
|
@ -37,12 +37,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
''' Determine permission class. '''
|
''' Determine permission class. '''
|
||||||
if self.action in [
|
if self.action in [
|
||||||
'update_layout',
|
'update_layout',
|
||||||
'create_operation',
|
|
||||||
'create_block',
|
'create_block',
|
||||||
|
'update_block',
|
||||||
|
'delete_block',
|
||||||
|
'move_items',
|
||||||
|
'create_operation',
|
||||||
|
'update_operation',
|
||||||
'delete_operation',
|
'delete_operation',
|
||||||
'create_input',
|
'create_input',
|
||||||
'set_input',
|
'set_input',
|
||||||
'update_operation',
|
|
||||||
'execute_operation',
|
'execute_operation',
|
||||||
'relocate_constituents'
|
'relocate_constituents'
|
||||||
]:
|
]:
|
||||||
|
@ -91,12 +94,168 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
|
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data)
|
||||||
return Response(status=c.HTTP_200_OK)
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='create block',
|
||||||
|
tags=['OSS'],
|
||||||
|
request=s.CreateBlockSerializer(),
|
||||||
|
responses={
|
||||||
|
c.HTTP_201_CREATED: s.BlockCreatedResponse,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['post'], url_path='create-block')
|
||||||
|
def create_block(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Create Block. '''
|
||||||
|
serializer = s.CreateBlockSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={'oss': self.get_object()}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
oss = m.OperationSchema(self.get_object())
|
||||||
|
layout = serializer.validated_data['layout']
|
||||||
|
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
|
||||||
|
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
|
||||||
|
with transaction.atomic():
|
||||||
|
new_block = oss.create_block(**serializer.validated_data['item_data'])
|
||||||
|
layout['blocks'].append({
|
||||||
|
'id': new_block.pk,
|
||||||
|
'x': serializer.validated_data['position_x'],
|
||||||
|
'y': serializer.validated_data['position_y'],
|
||||||
|
'width': serializer.validated_data['width'],
|
||||||
|
'height': serializer.validated_data['height'],
|
||||||
|
})
|
||||||
|
oss.update_layout(layout)
|
||||||
|
if len(children_blocks) > 0:
|
||||||
|
for block in children_blocks:
|
||||||
|
block.parent = new_block
|
||||||
|
m.Block.objects.bulk_update(children_blocks, ['parent'])
|
||||||
|
if len(children_operations) > 0:
|
||||||
|
for operation in children_operations:
|
||||||
|
operation.parent = new_block
|
||||||
|
m.Operation.objects.bulk_update(children_operations, ['parent'])
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_201_CREATED,
|
||||||
|
data={
|
||||||
|
'new_block': s.BlockSerializer(new_block).data,
|
||||||
|
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='update block',
|
||||||
|
tags=['OSS'],
|
||||||
|
request=s.UpdateBlockSerializer(),
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['patch'], url_path='update-block')
|
||||||
|
def update_block(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Update Block. '''
|
||||||
|
serializer = s.UpdateBlockSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={'oss': self.get_object()}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
block: m.Block = cast(m.Block, serializer.validated_data['target'])
|
||||||
|
oss = m.OperationSchema(self.get_object())
|
||||||
|
with transaction.atomic():
|
||||||
|
if 'layout' in serializer.validated_data:
|
||||||
|
oss.update_layout(serializer.validated_data['layout'])
|
||||||
|
if 'title' in serializer.validated_data['item_data']:
|
||||||
|
block.title = serializer.validated_data['item_data']['title']
|
||||||
|
if 'description' in serializer.validated_data['item_data']:
|
||||||
|
block.description = serializer.validated_data['item_data']['description']
|
||||||
|
if 'parent' in serializer.validated_data['item_data']:
|
||||||
|
block.parent = serializer.validated_data['item_data']['parent']
|
||||||
|
block.save(update_fields=['title', 'description', 'parent'])
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=s.OperationSchemaSerializer(oss.model).data
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='delete block',
|
||||||
|
tags=['OSS'],
|
||||||
|
request=s.DeleteBlockSerializer,
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['patch'], url_path='delete-block')
|
||||||
|
def delete_block(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Endpoint: Delete Block. '''
|
||||||
|
serializer = s.DeleteBlockSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={'oss': self.get_object()}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
oss = m.OperationSchema(self.get_object())
|
||||||
|
block = cast(m.Block, serializer.validated_data['target'])
|
||||||
|
layout = serializer.validated_data['layout']
|
||||||
|
layout['blocks'] = [x for x in layout['blocks'] if x['id'] != block.pk]
|
||||||
|
with transaction.atomic():
|
||||||
|
oss.delete_block(block)
|
||||||
|
oss.update_layout(layout)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=s.OperationSchemaSerializer(oss.model).data
|
||||||
|
)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
summary='move items',
|
||||||
|
tags=['OSS'],
|
||||||
|
request=s.MoveItemsSerializer(),
|
||||||
|
responses={
|
||||||
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=['patch'], url_path='move-items')
|
||||||
|
def move_items(self, request: Request, pk) -> HttpResponse:
|
||||||
|
''' Move items to another parent. '''
|
||||||
|
serializer = s.MoveItemsSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={'oss': self.get_object()}
|
||||||
|
)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
oss = m.OperationSchema(self.get_object())
|
||||||
|
with transaction.atomic():
|
||||||
|
oss.update_layout(serializer.validated_data['layout'])
|
||||||
|
for operation in serializer.validated_data['operations']:
|
||||||
|
operation.parent = serializer.validated_data['destination']
|
||||||
|
operation.save(update_fields=['parent'])
|
||||||
|
for block in serializer.validated_data['blocks']:
|
||||||
|
block.parent = serializer.validated_data['destination']
|
||||||
|
block.save(update_fields=['parent'])
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=c.HTTP_200_OK,
|
||||||
|
data=s.OperationSchemaSerializer(oss.model).data
|
||||||
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create operation',
|
summary='create operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.OperationCreateSerializer(),
|
request=s.CreateOperationSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_201_CREATED: s.NewOperationResponse,
|
c.HTTP_201_CREATED: s.OperationCreatedResponse,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
@ -104,8 +263,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='create-operation')
|
@action(detail=True, methods=['post'], url_path='create-operation')
|
||||||
def create_operation(self, request: Request, pk) -> HttpResponse:
|
def create_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create new operation. '''
|
''' Create Operation. '''
|
||||||
serializer = s.OperationCreateSerializer(
|
serializer = s.CreateOperationSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'oss': self.get_object()}
|
context={'oss': self.get_object()}
|
||||||
)
|
)
|
||||||
|
@ -153,60 +312,58 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create block',
|
summary='update operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.BlockCreateSerializer(),
|
request=s.UpdateOperationSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_201_CREATED: s.NewBlockResponse,
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='create-block')
|
@action(detail=True, methods=['patch'], url_path='update-operation')
|
||||||
def create_block(self, request: Request, pk) -> HttpResponse:
|
def update_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create new block. '''
|
''' Update Operation arguments and parameters. '''
|
||||||
serializer = s.BlockCreateSerializer(
|
serializer = s.UpdateOperationSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'oss': self.get_object()}
|
context={'oss': self.get_object()}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
oss = m.OperationSchema(self.get_object())
|
oss = m.OperationSchema(self.get_object())
|
||||||
layout = serializer.validated_data['layout']
|
|
||||||
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
|
|
||||||
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_block = oss.create_block(**serializer.validated_data['item_data'])
|
if 'layout' in serializer.validated_data:
|
||||||
layout['blocks'].append({
|
oss.update_layout(serializer.validated_data['layout'])
|
||||||
'id': new_block.pk,
|
if 'alias' in serializer.validated_data['item_data']:
|
||||||
'x': serializer.validated_data['position_x'],
|
operation.alias = serializer.validated_data['item_data']['alias']
|
||||||
'y': serializer.validated_data['position_y'],
|
if 'title' in serializer.validated_data['item_data']:
|
||||||
'width': serializer.validated_data['width'],
|
operation.title = serializer.validated_data['item_data']['title']
|
||||||
'height': serializer.validated_data['height'],
|
if 'description' in serializer.validated_data['item_data']:
|
||||||
})
|
operation.description = serializer.validated_data['item_data']['description']
|
||||||
oss.update_layout(layout)
|
operation.save(update_fields=['alias', 'title', 'description'])
|
||||||
if len(children_blocks) > 0:
|
|
||||||
for block in children_blocks:
|
|
||||||
block.parent = new_block
|
|
||||||
m.Block.objects.bulk_update(children_blocks, ['parent'])
|
|
||||||
if len(children_operations) > 0:
|
|
||||||
for operation in children_operations:
|
|
||||||
operation.parent = new_block
|
|
||||||
m.Operation.objects.bulk_update(children_operations, ['parent'])
|
|
||||||
|
|
||||||
|
if operation.result is not None:
|
||||||
|
can_edit = permissions.can_edit_item(request.user, operation.result)
|
||||||
|
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
|
||||||
|
operation.result.alias = operation.alias
|
||||||
|
operation.result.title = operation.title
|
||||||
|
operation.result.description = operation.description
|
||||||
|
operation.result.save()
|
||||||
|
if 'arguments' in serializer.validated_data:
|
||||||
|
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
|
||||||
|
if 'substitutions' in serializer.validated_data:
|
||||||
|
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data=s.OperationSchemaSerializer(oss.model).data
|
||||||
'new_block': s.BlockSerializer(new_block).data,
|
|
||||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='delete operation',
|
summary='delete operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.OperationDeleteSerializer,
|
request=s.DeleteOperationSerializer,
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
@ -216,8 +373,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='delete-operation')
|
@action(detail=True, methods=['patch'], url_path='delete-operation')
|
||||||
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete operation. '''
|
''' Endpoint: Delete Operation. '''
|
||||||
serializer = s.OperationDeleteSerializer(
|
serializer = s.DeleteOperationSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'oss': self.get_object()}
|
context={'oss': self.get_object()}
|
||||||
)
|
)
|
||||||
|
@ -246,9 +403,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create input schema for target operation',
|
summary='create input schema for target operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.OperationTargetSerializer(),
|
request=s.TargetOperationSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_200_OK: s.NewSchemaResponse,
|
c.HTTP_200_OK: s.SchemaCreatedResponse,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
c.HTTP_403_FORBIDDEN: None,
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
|
@ -256,8 +413,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='create-input')
|
@action(detail=True, methods=['patch'], url_path='create-input')
|
||||||
def create_input(self, request: Request, pk) -> HttpResponse:
|
def create_input(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create new input RSForm. '''
|
''' Create input RSForm. '''
|
||||||
serializer = s.OperationTargetSerializer(
|
serializer = s.TargetOperationSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'oss': self.get_object()}
|
context={'oss': self.get_object()}
|
||||||
)
|
)
|
||||||
|
@ -333,55 +490,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
data=s.OperationSchemaSerializer(oss.model).data
|
data=s.OperationSchemaSerializer(oss.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
|
||||||
summary='update operation',
|
|
||||||
tags=['OSS'],
|
|
||||||
request=s.OperationUpdateSerializer(),
|
|
||||||
responses={
|
|
||||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
|
||||||
c.HTTP_403_FORBIDDEN: None,
|
|
||||||
c.HTTP_404_NOT_FOUND: None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@action(detail=True, methods=['patch'], url_path='update-operation')
|
|
||||||
def update_operation(self, request: Request, pk) -> HttpResponse:
|
|
||||||
''' Update operation arguments and parameters. '''
|
|
||||||
serializer = s.OperationUpdateSerializer(
|
|
||||||
data=request.data,
|
|
||||||
context={'oss': self.get_object()}
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
|
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
|
||||||
oss = m.OperationSchema(self.get_object())
|
|
||||||
with transaction.atomic():
|
|
||||||
oss.update_layout(serializer.validated_data['layout'])
|
|
||||||
operation.alias = serializer.validated_data['item_data']['alias']
|
|
||||||
operation.title = serializer.validated_data['item_data']['title']
|
|
||||||
operation.description = serializer.validated_data['item_data']['description']
|
|
||||||
operation.save(update_fields=['alias', 'title', 'description'])
|
|
||||||
|
|
||||||
if operation.result is not None:
|
|
||||||
can_edit = permissions.can_edit_item(request.user, operation.result)
|
|
||||||
if can_edit or operation.operation_type == m.OperationType.SYNTHESIS:
|
|
||||||
operation.result.alias = operation.alias
|
|
||||||
operation.result.title = operation.title
|
|
||||||
operation.result.description = operation.description
|
|
||||||
operation.result.save()
|
|
||||||
if 'arguments' in serializer.validated_data:
|
|
||||||
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
|
|
||||||
if 'substitutions' in serializer.validated_data:
|
|
||||||
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
|
|
||||||
return Response(
|
|
||||||
status=c.HTTP_200_OK,
|
|
||||||
data=s.OperationSchemaSerializer(oss.model).data
|
|
||||||
)
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='execute operation',
|
summary='execute operation',
|
||||||
tags=['OSS'],
|
tags=['OSS'],
|
||||||
request=s.OperationTargetSerializer(),
|
request=s.TargetOperationSerializer(),
|
||||||
responses={
|
responses={
|
||||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||||
c.HTTP_400_BAD_REQUEST: None,
|
c.HTTP_400_BAD_REQUEST: None,
|
||||||
|
@ -392,7 +504,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
@action(detail=True, methods=['post'], url_path='execute-operation')
|
@action(detail=True, methods=['post'], url_path='execute-operation')
|
||||||
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Execute operation. '''
|
''' Execute operation. '''
|
||||||
serializer = s.OperationTargetSerializer(
|
serializer = s.TargetOperationSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'oss': self.get_object()}
|
context={'oss': self.get_object()}
|
||||||
)
|
)
|
||||||
|
|
|
@ -137,7 +137,7 @@ class RSForm:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||||
''' Create new cst from data. '''
|
''' Create constituenta from data. '''
|
||||||
if insert_after is None:
|
if insert_after is None:
|
||||||
position = INSERT_LAST
|
position = INSERT_LAST
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -77,7 +77,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='create-cst')
|
@action(detail=True, methods=['post'], url_path='create-cst')
|
||||||
def create_cst(self, request: Request, pk) -> HttpResponse:
|
def create_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create new constituenta. '''
|
''' Create Constituenta. '''
|
||||||
serializer = s.CstCreateSerializer(data=request.data)
|
serializer = s.CstCreateSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
|
@ -254,7 +254,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
||||||
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete multiple constituents. '''
|
''' Endpoint: Delete multiple Constituents. '''
|
||||||
model = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstListSerializer(
|
serializer = s.CstListSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
|
@ -284,7 +284,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='move-cst')
|
@action(detail=True, methods=['patch'], url_path='move-cst')
|
||||||
def move_cst(self, request: Request, pk) -> HttpResponse:
|
def move_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Move multiple constituents. '''
|
''' Endpoint: Move multiple Constituents. '''
|
||||||
model = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstMoveSerializer(
|
serializer = s.CstMoveSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
|
@ -334,7 +334,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['patch'], url_path='restore-order')
|
@action(detail=True, methods=['patch'], url_path='restore-order')
|
||||||
def restore_order(self, request: Request, pk) -> HttpResponse:
|
def restore_order(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Restore order based on types and term graph. '''
|
''' Endpoint: Restore order based on types and Term graph. '''
|
||||||
model = self._get_item()
|
model = self._get_item()
|
||||||
m.RSForm(model).restore_order()
|
m.RSForm(model).restore_order()
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -449,7 +449,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='check-constituenta')
|
@action(detail=True, methods=['post'], url_path='check-constituenta')
|
||||||
def check_constituenta(self, request: Request, pk) -> HttpResponse:
|
def check_constituenta(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Check RSLang expression against schema context. '''
|
''' Endpoint: Check RSLang expression against Schema context. '''
|
||||||
serializer = s.ConstituentaCheckSerializer(data=request.data)
|
serializer = s.ConstituentaCheckSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
expression = serializer.validated_data['definition_formal']
|
expression = serializer.validated_data['definition_formal']
|
||||||
|
@ -474,7 +474,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='resolve')
|
@action(detail=True, methods=['post'], url_path='resolve')
|
||||||
def resolve(self, request: Request, pk) -> HttpResponse:
|
def resolve(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Resolve references in text against schema terms context. '''
|
''' Endpoint: Resolve references in text against Schema terms context. '''
|
||||||
serializer = s.TextSerializer(data=request.data)
|
serializer = s.TextSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
text = serializer.validated_data['text']
|
text = serializer.validated_data['text']
|
||||||
|
@ -543,7 +543,7 @@ class TrsImportView(views.APIView):
|
||||||
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='create new RSForm empty or from file',
|
summary='create RSForm empty or from file',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=LibraryItemSerializer,
|
request=LibraryItemSerializer,
|
||||||
responses={
|
responses={
|
||||||
|
|
|
@ -7,15 +7,23 @@ def constituentaNotInRSform(title: str):
|
||||||
|
|
||||||
|
|
||||||
def constituentaNotFromOperation():
|
def constituentaNotFromOperation():
|
||||||
return f'Конституента не соответствую аргументам операции'
|
return 'Конституента не соответствую аргументам операции'
|
||||||
|
|
||||||
|
|
||||||
def operationNotInOSS(title: str):
|
def operationNotInOSS():
|
||||||
return f'Операция не принадлежит ОСС: {title}'
|
return 'Операция не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
||||||
|
def blockNotInOSS():
|
||||||
|
return 'Блок не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
||||||
def parentNotInOSS():
|
def parentNotInOSS():
|
||||||
return f'Родительский блок не принадлежит ОСС'
|
return 'Родительский блок не принадлежит ОСС'
|
||||||
|
|
||||||
|
|
||||||
|
def blockCyclicHierarchy():
|
||||||
|
return 'Попытка создания циклического вложения'
|
||||||
|
|
||||||
|
|
||||||
def childNotInOSS():
|
def childNotInOSS():
|
||||||
|
|
2807
rsconcept/frontend/package-lock.json
generated
2807
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -17,70 +17,70 @@
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@radix-ui/react-popover": "^1.1.7",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@tanstack/react-query": "^5.72.2",
|
"@tanstack/react-query": "^5.76.0",
|
||||||
"@tanstack/react-query-devtools": "^5.72.2",
|
"@tanstack/react-query-devtools": "^5.76.0",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-themes": "^4.23.10",
|
"@uiw/codemirror-themes": "^4.23.12",
|
||||||
"@uiw/react-codemirror": "^4.23.10",
|
"@uiw/react-codemirror": "^4.23.12",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.510.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.10",
|
"react-intl": "^7.1.11",
|
||||||
"react-router": "^7.5.0",
|
"react-router": "^7.6.0",
|
||||||
"react-scan": "^0.3.3",
|
"react-scan": "^0.3.3",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.28.1",
|
"react-tooltip": "^5.28.1",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.9",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.4",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.3",
|
"@lezer/generator": "^1.7.3",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.52.0",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.15.17",
|
||||||
"@types/react": "^19.1.1",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"eslint": "^9.24.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-playwright": "^2.2.0",
|
"eslint-plugin-playwright": "^2.2.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.1.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"stylelint": "^16.18.0",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-recommended": "^16.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.3.1",
|
"ts-jest": "^29.3.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.29.1",
|
"typescript-eslint": "^8.32.1",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { NavigationState } from './navigation/navigation-context';
|
import { NavigationState } from './navigation/navigation-context';
|
||||||
import { Footer } from './footer';
|
import { Footer } from './footer';
|
||||||
import { GlobalDialogs } from './global-dialogs';
|
import { GlobalDialogs } from './global-dialogs';
|
||||||
import { GlobalLoader } from './global-Loader';
|
import { GlobalLoader } from './global-loader';
|
||||||
import { ToasterThemed } from './global-toaster';
|
import { ToasterThemed } from './global-toaster';
|
||||||
import { GlobalTooltips } from './global-tooltips';
|
import { GlobalTooltips } from './global-tooltips';
|
||||||
import { MutationErrors } from './mutation-errors';
|
import { MutationErrors } from './mutation-errors';
|
||||||
|
@ -28,6 +28,7 @@ export function ApplicationLayout() {
|
||||||
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
|
||||||
<ToasterThemed
|
<ToasterThemed
|
||||||
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
|
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
|
||||||
|
aria-label='Оповещения'
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
pauseOnFocusLoss={false}
|
pauseOnFocusLoss={false}
|
||||||
|
@ -41,7 +42,7 @@ export function ApplicationLayout() {
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className='overflow-x-auto max-w-[100vw]'
|
className='overflow-x-auto max-w-[100dvw]'
|
||||||
style={{ maxHeight: viewportHeight }}
|
style={{ maxHeight: viewportHeight }}
|
||||||
inert={activeDialog !== null}
|
inert={activeDialog !== null}
|
||||||
>
|
>
|
||||||
|
|
|
@ -113,6 +113,21 @@ const DlgUploadRSForm = React.lazy(() =>
|
||||||
const DlgGraphParams = React.lazy(() =>
|
const DlgGraphParams = React.lazy(() =>
|
||||||
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
|
||||||
);
|
);
|
||||||
|
const DlgCreateBlock = React.lazy(() =>
|
||||||
|
import('@/features/oss/dialogs/dlg-create-block').then(module => ({
|
||||||
|
default: module.DlgCreateBlock
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const DlgEditBlock = React.lazy(() =>
|
||||||
|
import('@/features/oss/dialogs/dlg-edit-block').then(module => ({
|
||||||
|
default: module.DlgEditBlock
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const DlgOssSettings = React.lazy(() =>
|
||||||
|
import('@/features/oss/dialogs/dlg-oss-settings').then(module => ({
|
||||||
|
default: module.DlgOssSettings
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
export const GlobalDialogs = () => {
|
export const GlobalDialogs = () => {
|
||||||
const active = useDialogsStore(state => state.active);
|
const active = useDialogsStore(state => state.active);
|
||||||
|
@ -127,6 +142,10 @@ export const GlobalDialogs = () => {
|
||||||
return <DlgCreateCst />;
|
return <DlgCreateCst />;
|
||||||
case DialogType.CREATE_OPERATION:
|
case DialogType.CREATE_OPERATION:
|
||||||
return <DlgCreateOperation />;
|
return <DlgCreateOperation />;
|
||||||
|
case DialogType.CREATE_BLOCK:
|
||||||
|
return <DlgCreateBlock />;
|
||||||
|
case DialogType.EDIT_BLOCK:
|
||||||
|
return <DlgEditBlock />;
|
||||||
case DialogType.DELETE_CONSTITUENTA:
|
case DialogType.DELETE_CONSTITUENTA:
|
||||||
return <DlgDeleteCst />;
|
return <DlgDeleteCst />;
|
||||||
case DialogType.EDIT_EDITORS:
|
case DialogType.EDIT_EDITORS:
|
||||||
|
@ -141,6 +160,8 @@ export const GlobalDialogs = () => {
|
||||||
return <DlgEditWordForms />;
|
return <DlgEditWordForms />;
|
||||||
case DialogType.INLINE_SYNTHESIS:
|
case DialogType.INLINE_SYNTHESIS:
|
||||||
return <DlgInlineSynthesis />;
|
return <DlgInlineSynthesis />;
|
||||||
|
case DialogType.OSS_SETTINGS:
|
||||||
|
return <DlgOssSettings />;
|
||||||
case DialogType.SHOW_AST:
|
case DialogType.SHOW_AST:
|
||||||
return <DlgShowAST />;
|
return <DlgShowAST />;
|
||||||
case DialogType.SHOW_TYPE_GRAPH:
|
case DialogType.SHOW_TYPE_GRAPH:
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const GlobalTooltips = () => {
|
||||||
id={globalIDs.tooltip}
|
id={globalIDs.tooltip}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='right-start'
|
place='right-start'
|
||||||
className='mt-8 max-w-80 break-words'
|
className='mt-8 max-w-80 break-words rounded-lg!'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
float
|
float
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
import { useMutationErrors } from '@/backend/use-mutation-errors';
|
||||||
import { Button } from '@/components/control';
|
import { Button } from '@/components/control';
|
||||||
import { DescribeError } from '@/components/info-error';
|
import { DescribeError } from '@/components/info-error';
|
||||||
|
@ -20,9 +22,23 @@ export function MutationErrors() {
|
||||||
return (
|
return (
|
||||||
<div className='cc-modal-wrapper'>
|
<div className='cc-modal-wrapper'>
|
||||||
<ModalBackdrop onHide={resetErrors} />
|
<ModalBackdrop onHide={resetErrors} />
|
||||||
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-background' role='alertdialog'>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'z-pop', //
|
||||||
|
'flex flex-col px-10 py-3 items-center',
|
||||||
|
'border rounded-xl bg-background'
|
||||||
|
)}
|
||||||
|
role='alertdialog'
|
||||||
|
>
|
||||||
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
|
||||||
<div className='px-3 flex flex-col text-destructive text-sm font-semibold select-text'>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'max-h-[calc(100svh-8rem)] max-w-[calc(100svw-2rem)]',
|
||||||
|
'px-3 flex flex-col',
|
||||||
|
'text-destructive text-sm font-semibold select-text',
|
||||||
|
'overflow-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<DescribeError error={mutationErrors[0]} />
|
<DescribeError error={mutationErrors[0]} />
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
<Button onClick={resetErrors} className='w-fit' text='Закрыть' />
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function Navigation() {
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'pl-2 sm:pr-4 h-12 flex cc-shadow-border',
|
'pl-2 sm:pr-4 h-12 flex cc-shadow-border',
|
||||||
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
|
'transition-[max-height,translate] ease-bezier duration-move',
|
||||||
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
|
|
||||||
import { buildConstants } from '@/utils/build-constants';
|
import { buildConstants } from '@/utils/build-constants';
|
||||||
|
|
||||||
/**
|
/** Routes. */
|
||||||
* Routes.
|
|
||||||
*/
|
|
||||||
export const routes = {
|
export const routes = {
|
||||||
not_found: 'not-found',
|
not_found: 'not-found',
|
||||||
login: 'login',
|
login: 'login',
|
||||||
|
@ -22,11 +20,9 @@ export const routes = {
|
||||||
oss: 'oss',
|
oss: 'oss',
|
||||||
icons: 'icons',
|
icons: 'icons',
|
||||||
database_schema: 'database-schema'
|
database_schema: 'database-schema'
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
/**
|
/** Internal navigation URLs. */
|
||||||
* Internal navigation URLs.
|
|
||||||
*/
|
|
||||||
export const urls = {
|
export const urls = {
|
||||||
page404: '/not-found',
|
page404: '/not-found',
|
||||||
admin: `${buildConstants.backend}/admin`,
|
admin: `${buildConstants.backend}/admin`,
|
||||||
|
@ -66,4 +62,4 @@ export const urls = {
|
||||||
oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
|
oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
|
||||||
return `/oss/${id}?tab=${tab}`;
|
return `/oss/${id}?tab=${tab}`;
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { type z, ZodError } from 'zod';
|
||||||
import { buildConstants } from '@/utils/build-constants';
|
import { buildConstants } from '@/utils/build-constants';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
import { extractErrorMessage } from '@/utils/utils';
|
import { extractErrorMessage } from '@/utils/utils';
|
||||||
|
|
||||||
export { AxiosError } from 'axios';
|
export { AxiosError } from 'axios';
|
||||||
|
@ -58,7 +59,7 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
|
||||||
.get<ResponseData>(endpoint, options)
|
.get<ResponseData>(endpoint, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
return response.data;
|
return response.data as RO<ResponseData>;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
// Note: Ignore cancellation errors
|
// Note: Ignore cancellation errors
|
||||||
|
@ -81,7 +82,7 @@ export function axiosPost<RequestData, ResponseData = void>({
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
notifySuccess(response.data, request?.successMessage);
|
notifySuccess(response.data, request?.successMessage);
|
||||||
return response.data;
|
return response.data as RO<ResponseData>;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
notifyError(error);
|
notifyError(error);
|
||||||
|
@ -100,7 +101,7 @@ export function axiosDelete<RequestData, ResponseData = void>({
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
notifySuccess(response.data, request?.successMessage);
|
notifySuccess(response.data, request?.successMessage);
|
||||||
return response.data;
|
return response.data as RO<ResponseData>;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
notifyError(error);
|
notifyError(error);
|
||||||
|
@ -119,7 +120,7 @@ export function axiosPatch<RequestData, ResponseData = void>({
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
notifySuccess(response.data, request?.successMessage);
|
notifySuccess(response.data, request?.successMessage);
|
||||||
return response.data;
|
return response.data as RO<ResponseData>;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
notifyError(error);
|
notifyError(error);
|
||||||
|
|
|
@ -6,7 +6,7 @@ export const DELAYS = {
|
||||||
staleShort: 5 * 60 * 1000,
|
staleShort: 5 * 60 * 1000,
|
||||||
staleMedium: 1 * 60 * 60 * 1000,
|
staleMedium: 1 * 60 * 60 * 1000,
|
||||||
staleLong: 24 * 60 * 60 * 1000
|
staleLong: 24 * 60 * 60 * 1000
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
/** API keys for local cache. */
|
/** API keys for local cache. */
|
||||||
export const KEYS = {
|
export const KEYS = {
|
||||||
|
@ -19,8 +19,8 @@ export const KEYS = {
|
||||||
global_mutation: 'global_mutation',
|
global_mutation: 'global_mutation',
|
||||||
|
|
||||||
composite: {
|
composite: {
|
||||||
libraryList: ['library', 'list'],
|
libraryList: ['library', 'list'] as const,
|
||||||
ossItem: ({ itemID }: { itemID?: number }) => [KEYS.oss, 'item', itemID],
|
ossItem: ({ itemID }: { itemID?: number }) => [KEYS.oss, 'item', itemID],
|
||||||
rsItem: ({ itemID, version }: { itemID?: number; version?: number }) => [KEYS.rsform, 'item', itemID, version ?? '']
|
rsItem: ({ itemID, version }: { itemID?: number; version?: number }) => [KEYS.rsform, 'item', itemID, version ?? '']
|
||||||
}
|
}
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -19,19 +19,10 @@ import { PaginationTools } from './pagination-tools';
|
||||||
import { TableBody } from './table-body';
|
import { TableBody } from './table-body';
|
||||||
import { TableFooter } from './table-footer';
|
import { TableFooter } from './table-footer';
|
||||||
import { TableHeader } from './table-header';
|
import { TableHeader } from './table-header';
|
||||||
import { useDataTable } from './use-data-table';
|
import { type IConditionalStyle, useDataTable } from './use-data-table';
|
||||||
|
|
||||||
export { createColumnHelper, type RowSelectionState, type VisibilityState };
|
export { createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||||
|
|
||||||
/** Style to conditionally apply to rows. */
|
|
||||||
export interface IConditionalStyle<TData> {
|
|
||||||
/** Callback to determine if the style should be applied. */
|
|
||||||
when: (rowData: TData) => boolean;
|
|
||||||
|
|
||||||
/** Style to apply. */
|
|
||||||
style: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataTableProps<TData extends RowData>
|
export interface DataTableProps<TData extends RowData>
|
||||||
extends Styling,
|
extends Styling,
|
||||||
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||||
|
@ -87,7 +78,7 @@ export interface DataTableProps<TData extends RowData>
|
||||||
paginationPerPage?: number;
|
paginationPerPage?: number;
|
||||||
|
|
||||||
/** List of options to choose from for pagination. */
|
/** List of options to choose from for pagination. */
|
||||||
paginationOptions?: number[];
|
paginationOptions?: readonly number[];
|
||||||
|
|
||||||
/** Callback to be called when the pagination option is changed. */
|
/** Callback to be called when the pagination option is changed. */
|
||||||
onChangePaginationOption?: (newValue: number) => void;
|
onChangePaginationOption?: (newValue: number) => void;
|
||||||
|
|
|
@ -1,7 +1,2 @@
|
||||||
export {
|
export { createColumnHelper, DataTable, type RowSelectionState, type VisibilityState } from './data-table';
|
||||||
createColumnHelper,
|
export { type IConditionalStyle } from './use-data-table';
|
||||||
DataTable,
|
|
||||||
type IConditionalStyle,
|
|
||||||
type RowSelectionState,
|
|
||||||
type VisibilityState
|
|
||||||
} from './data-table';
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
'use no memo';
|
'use no memo';
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { type Table } from '@tanstack/react-table';
|
import { type Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
|
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
|
||||||
|
|
||||||
|
import { SelectPagination } from './select-pagination';
|
||||||
|
|
||||||
interface PaginationToolsProps<TData> {
|
interface PaginationToolsProps<TData> {
|
||||||
id?: string;
|
id?: string;
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
paginationOptions: number[];
|
paginationOptions: readonly number[];
|
||||||
onChangePaginationOption?: (newValue: number) => void;
|
onChangePaginationOption?: (newValue: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,15 +20,6 @@ export function PaginationTools<TData>({
|
||||||
onChangePaginationOption,
|
onChangePaginationOption,
|
||||||
paginationOptions
|
paginationOptions
|
||||||
}: PaginationToolsProps<TData>) {
|
}: PaginationToolsProps<TData>) {
|
||||||
const handlePaginationOptionsChange = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
const perPage = Number(event.target.value);
|
|
||||||
table.setPageSize(perPage);
|
|
||||||
onChangePaginationOption?.(perPage);
|
|
||||||
},
|
|
||||||
[table, onChangePaginationOption]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
|
<div className='flex justify-end items-center my-2 text-sm cc-controls select-none'>
|
||||||
<span className='mr-3'>
|
<span className='mr-3'>
|
||||||
|
@ -93,19 +83,12 @@ export function PaginationTools<TData>({
|
||||||
<IconPageLast size='1.5rem' />
|
<IconPageLast size='1.5rem' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<SelectPagination
|
||||||
id={id ? `${id}__per_page` : undefined}
|
id={id ? `${id}__per_page` : undefined}
|
||||||
aria-label='Выбор количества строчек на странице'
|
table={table}
|
||||||
value={table.getState().pagination.pageSize}
|
paginationOptions={paginationOptions}
|
||||||
onChange={handlePaginationOptionsChange}
|
onChange={onChangePaginationOption}
|
||||||
className='mx-2 cursor-pointer bg-transparent focus-outline'
|
/>
|
||||||
>
|
|
||||||
{paginationOptions.map(pageSize => (
|
|
||||||
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize} aria-label={`${pageSize} на страницу`}>
|
|
||||||
{pageSize} на стр
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use no memo';
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { type Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../input/select';
|
||||||
|
|
||||||
|
interface SelectPaginationProps<TData> {
|
||||||
|
id?: string;
|
||||||
|
table: Table<TData>;
|
||||||
|
paginationOptions: readonly number[];
|
||||||
|
onChange?: (newValue: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectPagination<TData>({ id, table, paginationOptions, onChange }: SelectPaginationProps<TData>) {
|
||||||
|
const handlePaginationOptionsChange = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
|
const perPage = Number(newValue);
|
||||||
|
table.setPageSize(perPage);
|
||||||
|
onChange?.(perPage);
|
||||||
|
},
|
||||||
|
[table, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={handlePaginationOptionsChange} defaultValue={String(table.getState().pagination.pageSize)}>
|
||||||
|
<SelectTrigger
|
||||||
|
id={id}
|
||||||
|
aria-label='Выбор количества строчек на странице'
|
||||||
|
className='mx-2 cursor-pointer bg-transparent focus-outline border-0 w-28 max-h-6 px-2 justify-end'
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{paginationOptions?.map(option => (
|
||||||
|
<SelectItem key={`${prefixes.page_size}${option}`} value={String(option)}>
|
||||||
|
{option} на стр
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
'use no memo';
|
'use no memo';
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
|
import { type Row, type Table } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { SelectRow } from './select-row';
|
import { TableRow } from './table-row';
|
||||||
import { type IConditionalStyle } from '.';
|
import { type IConditionalStyle } from './use-data-table';
|
||||||
|
|
||||||
interface TableBodyProps<TData> {
|
interface TableBodyProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
|
@ -30,82 +30,43 @@ export function TableBody<TData>({
|
||||||
onRowClicked,
|
onRowClicked,
|
||||||
onRowDoubleClicked
|
onRowDoubleClicked
|
||||||
}: TableBodyProps<TData>) {
|
}: TableBodyProps<TData>) {
|
||||||
const handleRowClicked = useCallback(
|
const getRowStyles = useCallback(
|
||||||
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
(row: Row<TData>) =>
|
||||||
onRowClicked?.(target.original, event);
|
conditionalRowStyles
|
||||||
if (table.options.enableRowSelection && target.getCanSelect()) {
|
?.filter(item => !!item.style && item.when(row.original))
|
||||||
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
?.reduce((prev, item) => ({ ...prev, ...item.style }), {}),
|
||||||
const { rows, rowsById } = table.getRowModel();
|
|
||||||
const lastIndex = rowsById[lastSelected].index;
|
[conditionalRowStyles]
|
||||||
const currentIndex = target.index;
|
|
||||||
const toggleRows = rows.slice(
|
|
||||||
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
|
|
||||||
lastIndex > currentIndex ? lastIndex : currentIndex + 1
|
|
||||||
);
|
|
||||||
const newSelection: Record<string, boolean> = {};
|
|
||||||
toggleRows.forEach(row => {
|
|
||||||
newSelection[row.id] = !target.getIsSelected();
|
|
||||||
});
|
|
||||||
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
|
||||||
onChangeLastSelected(null);
|
|
||||||
} else {
|
|
||||||
onChangeLastSelected(target.id);
|
|
||||||
target.toggleSelected(!target.getIsSelected());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[table, lastSelected, onChangeLastSelected, onRowClicked]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRowStyles = useCallback(
|
const getRowClasses = useCallback(
|
||||||
(row: Row<TData>) => {
|
(row: Row<TData>) => {
|
||||||
return {
|
return conditionalRowStyles
|
||||||
...conditionalRowStyles!
|
?.filter(item => !!item.className && item.when(row.original))
|
||||||
.filter(item => item.when(row.original))
|
?.reduce((prev, item) => {
|
||||||
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
|
prev.push(item.className!);
|
||||||
};
|
return prev;
|
||||||
|
}, [] as string[]);
|
||||||
},
|
},
|
||||||
[conditionalRowStyles]
|
[conditionalRowStyles]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row: Row<TData>, index) => (
|
{table.getRowModel().rows.map((row: Row<TData>) => (
|
||||||
<tr
|
<TableRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className={clsx(
|
table={table}
|
||||||
'cc-scroll-row',
|
row={row}
|
||||||
'cc-hover cc-animate-background duration-(--duration-fade)',
|
className={getRowClasses(row)?.join(' ')}
|
||||||
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
style={conditionalRowStyles ? { ...getRowStyles(row) } : undefined}
|
||||||
table.options.enableRowSelection && row.getIsSelected()
|
noHeader={noHeader}
|
||||||
? 'cc-selected'
|
dense={dense}
|
||||||
: 'odd:bg-secondary even:bg-background'
|
lastSelected={lastSelected}
|
||||||
)}
|
onChangeLastSelected={onChangeLastSelected}
|
||||||
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
|
onRowClicked={onRowClicked}
|
||||||
onClick={event => handleRowClicked(row, event)}
|
onRowDoubleClicked={onRowDoubleClicked}
|
||||||
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
/>
|
||||||
>
|
|
||||||
{table.options.enableRowSelection ? (
|
|
||||||
<td key={`select-${row.id}`} className='pl-2 border-y'>
|
|
||||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
|
||||||
</td>
|
|
||||||
) : null}
|
|
||||||
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
|
||||||
<td
|
|
||||||
key={cell.id}
|
|
||||||
className={clsx(
|
|
||||||
'px-2 align-middle border-y',
|
|
||||||
dense ? 'py-1' : 'py-2',
|
|
||||||
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
'use no memo';
|
'use no memo';
|
||||||
|
'use client';
|
||||||
|
|
||||||
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
108
rsconcept/frontend/src/components/data-table/table-row.tsx
Normal file
108
rsconcept/frontend/src/components/data-table/table-row.tsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
'use no memo';
|
||||||
|
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { cn } from '../utils';
|
||||||
|
|
||||||
|
import { SelectRow } from './select-row';
|
||||||
|
|
||||||
|
interface TableRowProps<TData> {
|
||||||
|
table: Table<TData>;
|
||||||
|
row: Row<TData>;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
|
noHeader?: boolean;
|
||||||
|
dense?: boolean;
|
||||||
|
|
||||||
|
lastSelected: string | null;
|
||||||
|
onChangeLastSelected: (newValue: string | null) => void;
|
||||||
|
|
||||||
|
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||||
|
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableRow<TData>({
|
||||||
|
table,
|
||||||
|
row,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
noHeader,
|
||||||
|
dense,
|
||||||
|
lastSelected,
|
||||||
|
onChangeLastSelected,
|
||||||
|
onRowClicked,
|
||||||
|
onRowDoubleClicked
|
||||||
|
}: TableRowProps<TData>) {
|
||||||
|
const hasBG = className?.includes('bg-') ?? false;
|
||||||
|
|
||||||
|
const handleRowClicked = useCallback(
|
||||||
|
(target: Row<TData>, event: React.MouseEvent<Element>) => {
|
||||||
|
onRowClicked?.(target.original, event);
|
||||||
|
if (table.options.enableRowSelection && target.getCanSelect()) {
|
||||||
|
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
|
||||||
|
const { rows, rowsById } = table.getRowModel();
|
||||||
|
const lastIndex = rowsById[lastSelected].index;
|
||||||
|
const currentIndex = target.index;
|
||||||
|
const toggleRows = rows.slice(
|
||||||
|
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
|
||||||
|
lastIndex > currentIndex ? lastIndex : currentIndex + 1
|
||||||
|
);
|
||||||
|
const newSelection: Record<string, boolean> = {};
|
||||||
|
toggleRows.forEach(row => {
|
||||||
|
newSelection[row.id] = !target.getIsSelected();
|
||||||
|
});
|
||||||
|
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
||||||
|
onChangeLastSelected(null);
|
||||||
|
} else {
|
||||||
|
onChangeLastSelected(target.id);
|
||||||
|
target.toggleSelected(!target.getIsSelected());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[table, lastSelected, onChangeLastSelected, onRowClicked]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className={cn(
|
||||||
|
'cc-scroll-row',
|
||||||
|
'cc-hover cc-animate-background duration-fade',
|
||||||
|
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
|
||||||
|
table.options.enableRowSelection && row.getIsSelected()
|
||||||
|
? 'cc-selected'
|
||||||
|
: !hasBG
|
||||||
|
? 'odd:bg-secondary even:bg-background'
|
||||||
|
: '',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
onClick={event => handleRowClicked(row, event)}
|
||||||
|
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
|
||||||
|
>
|
||||||
|
{table.options.enableRowSelection ? (
|
||||||
|
<td key={`select-${row.id}`} className='pl-2 border-y'>
|
||||||
|
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={clsx(
|
||||||
|
'px-2 align-middle border-y',
|
||||||
|
dense ? 'py-1' : 'py-2',
|
||||||
|
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: noHeader && row.index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
|
@ -21,7 +21,10 @@ export interface IConditionalStyle<TData> {
|
||||||
when: (rowData: TData) => boolean;
|
when: (rowData: TData) => boolean;
|
||||||
|
|
||||||
/** Style to apply. */
|
/** Style to apply. */
|
||||||
style: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
|
/** Classname to apply. */
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseDataTableProps<TData extends RowData>
|
interface UseDataTableProps<TData extends RowData>
|
||||||
|
|
100
rsconcept/frontend/src/components/flow/diagram-flow.tsx
Normal file
100
rsconcept/frontend/src/components/flow/diagram-flow.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type ReactNode, useState } from 'react';
|
||||||
|
import { Background, ReactFlow, type ReactFlowProps } from 'reactflow';
|
||||||
|
|
||||||
|
export { useReactFlow, useStoreApi } from 'reactflow';
|
||||||
|
|
||||||
|
import { cn } from '../utils';
|
||||||
|
|
||||||
|
type DiagramFlowProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
height?: number | string;
|
||||||
|
showGrid?: boolean;
|
||||||
|
gridSize?: number;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
onKeyUp?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||||
|
} & ReactFlowProps;
|
||||||
|
|
||||||
|
export function DiagramFlow({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
height,
|
||||||
|
showGrid = false,
|
||||||
|
gridSize = 20,
|
||||||
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
|
|
||||||
|
nodesDraggable = true,
|
||||||
|
nodesFocusable,
|
||||||
|
edgesFocusable,
|
||||||
|
|
||||||
|
onNodesChange,
|
||||||
|
onEdgesChange,
|
||||||
|
onNodeClick,
|
||||||
|
onNodeDoubleClick,
|
||||||
|
onNodeContextMenu,
|
||||||
|
onNodeMouseEnter,
|
||||||
|
onNodeMouseLeave,
|
||||||
|
|
||||||
|
onNodeDragStart,
|
||||||
|
onNodeDrag,
|
||||||
|
onNodeDragStop,
|
||||||
|
onContextMenu,
|
||||||
|
...restProps
|
||||||
|
}: DiagramFlowProps) {
|
||||||
|
const [spaceMode, setSpaceMode] = useState(false);
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setSpaceMode(true);
|
||||||
|
}
|
||||||
|
onKeyDown?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
if (event.code === 'Space') {
|
||||||
|
setSpaceMode(false);
|
||||||
|
}
|
||||||
|
onKeyUp?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
onContextMenu?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cn('relative cc-mask-sides max-w-480 w-[100dvw]', spaceMode && 'space-mode', className)}
|
||||||
|
style={{ ...style, height: height }}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
<ReactFlow
|
||||||
|
{...restProps}
|
||||||
|
onNodesChange={spaceMode ? undefined : onNodesChange}
|
||||||
|
onEdgesChange={spaceMode ? undefined : onEdgesChange}
|
||||||
|
onNodeClick={spaceMode ? undefined : onNodeClick}
|
||||||
|
onNodeDoubleClick={spaceMode ? undefined : onNodeDoubleClick}
|
||||||
|
nodesDraggable={!spaceMode && nodesDraggable}
|
||||||
|
nodesFocusable={!spaceMode && nodesFocusable}
|
||||||
|
edgesFocusable={!spaceMode && edgesFocusable}
|
||||||
|
onNodeDragStart={spaceMode ? undefined : onNodeDragStart}
|
||||||
|
onNodeDrag={spaceMode ? undefined : onNodeDrag}
|
||||||
|
onNodeDragStop={spaceMode ? undefined : onNodeDragStop}
|
||||||
|
onNodeContextMenu={spaceMode ? undefined : onNodeContextMenu}
|
||||||
|
onNodeMouseEnter={spaceMode ? undefined : onNodeMouseEnter}
|
||||||
|
onNodeMouseLeave={spaceMode ? undefined : onNodeMouseLeave}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
>
|
||||||
|
{showGrid ? <Background gap={gridSize} /> : null}
|
||||||
|
{children}
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,12 +10,13 @@ export { BiCheck as IconAccept } from 'react-icons/bi';
|
||||||
export { BiX as IconRemove } from 'react-icons/bi';
|
export { BiX as IconRemove } from 'react-icons/bi';
|
||||||
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
export { BiTrash as IconDestroy } from 'react-icons/bi';
|
||||||
export { BiReset as IconReset } from 'react-icons/bi';
|
export { BiReset as IconReset } from 'react-icons/bi';
|
||||||
|
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
|
||||||
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
export { LiaEdit as IconEdit } from 'react-icons/lia';
|
||||||
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
||||||
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||||
export { BiDownload as IconDownload } from 'react-icons/bi';
|
export { BiDownload as IconDownload } from 'react-icons/bi';
|
||||||
export { BiUpload as IconUpload } from 'react-icons/bi';
|
export { BiUpload as IconUpload } from 'react-icons/bi';
|
||||||
export { BiCog as IconSettings } from 'react-icons/bi';
|
export { LuSettings as IconSettings } from 'react-icons/lu';
|
||||||
export { TbEye as IconShow } from 'react-icons/tb';
|
export { TbEye as IconShow } from 'react-icons/tb';
|
||||||
export { TbEyeX as IconHide } from 'react-icons/tb';
|
export { TbEyeX as IconHide } from 'react-icons/tb';
|
||||||
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
||||||
|
@ -68,9 +69,12 @@ export { LuGlasses as IconReader } from 'react-icons/lu';
|
||||||
// ===== Domain entities =======
|
// ===== Domain entities =======
|
||||||
export { TbBriefcase as IconBusiness } from 'react-icons/tb';
|
export { TbBriefcase as IconBusiness } from 'react-icons/tb';
|
||||||
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
export { VscLibrary as IconLibrary } from 'react-icons/vsc';
|
||||||
|
export { BiBot as IconRobot } from 'react-icons/bi';
|
||||||
|
export { TbGps as IconCoordinates } from 'react-icons/tb';
|
||||||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
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 { BiScreenshot as IconConceptBlock } from 'react-icons/bi';
|
||||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||||
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
||||||
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
||||||
|
@ -101,7 +105,7 @@ export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||||
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
||||||
export { BiFontFamily as IconText } from 'react-icons/bi';
|
export { BiFontFamily as IconText } from 'react-icons/bi';
|
||||||
export { BiFont as IconTextOff } from 'react-icons/bi';
|
export { BiFont as IconTextOff } from 'react-icons/bi';
|
||||||
export { TbCar4Wd as IconTypeGraph } from 'react-icons/tb';
|
export { TbCircleLetterM as IconTypeGraph } from 'react-icons/tb';
|
||||||
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
export { RiTreeLine as IconTree } from 'react-icons/ri';
|
||||||
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
|
||||||
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
export { RiLockLine as IconImmutable } from 'react-icons/ri';
|
||||||
|
@ -137,6 +141,7 @@ export { GrConnect as IconConnect } from 'react-icons/gr';
|
||||||
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
||||||
|
|
||||||
// ======== Graph UI =======
|
// ======== Graph UI =======
|
||||||
|
export { LuLayoutDashboard as IconFixLayout } from 'react-icons/lu';
|
||||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||||
|
@ -200,7 +205,7 @@ export function IconLogin(props: IconProps) {
|
||||||
|
|
||||||
export function CheckboxChecked() {
|
export function CheckboxChecked() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-4 h-4 p-0.75 -ml-0.25' viewBox='0 0 512 512' fill='#ffffff'>
|
<svg className='w-4 h-4 p-0.75 -ml-0.25 -mt-0.25' viewBox='0 0 512 512' fill='#ffffff'>
|
||||||
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -208,7 +213,7 @@ export function CheckboxChecked() {
|
||||||
|
|
||||||
export function CheckboxNull() {
|
export function CheckboxNull() {
|
||||||
return (
|
return (
|
||||||
<svg className='w-4 h-4 px-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
<svg className='w-4 h-4 px-0.25 -ml-0.25 -mt-0.25' viewBox='0 0 16 16' fill='#ffffff'>
|
||||||
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
|
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,8 +78,7 @@ export function InfoError({ error }: InfoErrorProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-fade-in',
|
'min-w-100', //
|
||||||
'min-w-100',
|
|
||||||
'px-3 py-2 flex flex-col',
|
'px-3 py-2 flex flex-col',
|
||||||
'text-destructive text-sm font-semibold',
|
'text-destructive text-sm font-semibold',
|
||||||
'select-text'
|
'select-text'
|
||||||
|
|
|
@ -14,10 +14,13 @@ export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onCha
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
/** Current value - `true` or `false`. */
|
/** Current value - `true` or `false`. */
|
||||||
value?: boolean;
|
value: boolean;
|
||||||
|
|
||||||
/** Callback to set the `value`. */
|
/** Callback to set the `value`. */
|
||||||
onChange?: (newValue: boolean) => void;
|
onChange?: (newValue: boolean) => void;
|
||||||
|
|
||||||
|
/** Custom icon to display next instead of checkbox. */
|
||||||
|
customIcon?: (checked?: boolean) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +34,7 @@ export function Checkbox({
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
|
customIcon,
|
||||||
onChange,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
|
@ -63,6 +67,9 @@ export function Checkbox({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
|
{customIcon ? (
|
||||||
|
customIcon(value)
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4', //
|
'w-4 h-4', //
|
||||||
|
@ -72,6 +79,7 @@ export function Checkbox({
|
||||||
>
|
>
|
||||||
{value ? <CheckboxChecked /> : null}
|
{value ? <CheckboxChecked /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
|
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { IconRemove } from '../icons';
|
import { IconRemove } from '../icons';
|
||||||
|
@ -11,7 +11,7 @@ import { cn } from '../utils';
|
||||||
|
|
||||||
interface ComboBoxProps<Option> extends Styling {
|
interface ComboBoxProps<Option> extends Styling {
|
||||||
id?: string;
|
id?: string;
|
||||||
items?: Option[];
|
items?: readonly Option[];
|
||||||
value: Option | null;
|
value: Option | null;
|
||||||
onChange: (newValue: Option | null) => void;
|
onChange: (newValue: Option | null) => void;
|
||||||
|
|
||||||
|
@ -49,11 +49,12 @@ export function ComboBox<Option>({
|
||||||
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function handleOpenChange(isOpen: boolean) {
|
||||||
if (triggerRef.current) {
|
setOpen(isOpen);
|
||||||
|
if (isOpen && triggerRef.current) {
|
||||||
setPopoverWidth(triggerRef.current.offsetWidth);
|
setPopoverWidth(triggerRef.current.offsetWidth);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}
|
||||||
|
|
||||||
function handleChangeValue(newValue: Option | null) {
|
function handleChangeValue(newValue: Option | null) {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
|
@ -66,7 +67,7 @@ export function ComboBox<Option>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { IconRemove } from '../icons';
|
import { IconRemove } from '../icons';
|
||||||
|
@ -43,11 +43,12 @@ export function ComboMulti<Option>({
|
||||||
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function handleOpenChange(isOpen: boolean) {
|
||||||
if (triggerRef.current) {
|
setOpen(isOpen);
|
||||||
|
if (isOpen && triggerRef.current) {
|
||||||
setPopoverWidth(triggerRef.current.offsetWidth);
|
setPopoverWidth(triggerRef.current.offsetWidth);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}
|
||||||
|
|
||||||
function handleAddValue(newValue: Option) {
|
function handleAddValue(newValue: Option) {
|
||||||
if (value.includes(newValue)) {
|
if (value.includes(newValue)) {
|
||||||
|
@ -70,7 +71,7 @@ export function ComboMulti<Option>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function SearchBar({
|
||||||
id={id}
|
id={id}
|
||||||
type='search'
|
type='search'
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-0 py-2',
|
'min-w-0 py-2 w-full pr-2',
|
||||||
'leading-tight truncate hover:text-clip',
|
'leading-tight truncate hover:text-clip',
|
||||||
'bg-transparent',
|
'bg-transparent',
|
||||||
!noIcon && 'pl-8',
|
!noIcon && 'pl-8',
|
||||||
|
|
|
@ -44,11 +44,12 @@ export function SelectTree<ItemType>({
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectTreeProps<ItemType>) {
|
}: SelectTreeProps<ItemType>) {
|
||||||
const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
|
const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
|
||||||
const [folded, setFolded] = useState<ItemType[]>(items);
|
const defaultFolded = items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item);
|
||||||
|
const [folded, setFolded] = useState<ItemType[]>(defaultFolded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
setFolded(defaultFolded);
|
||||||
}, [value, getParent, items]);
|
}, [defaultFolded]);
|
||||||
|
|
||||||
function onFoldItem(target: ItemType) {
|
function onFoldItem(target: ItemType) {
|
||||||
setFolded(prev =>
|
setFolded(prev =>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
|
||||||
|
|
||||||
interface LoaderProps {
|
interface LoaderProps {
|
||||||
/** Scale of the loader from 1 to 10. */
|
/** Scale of the loader from 1 to 10. */
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
@ -57,8 +55,8 @@ const animatePulse = (startBig: boolean, duration: string) => {
|
||||||
export function Loader({ scale = 5, circular }: LoaderProps) {
|
export function Loader({ scale = 5, circular }: LoaderProps) {
|
||||||
if (circular) {
|
if (circular) {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center' aria-label='three-circles-loading' aria-busy='true' role='progressbar'>
|
<div className='flex justify-center text-primary' aria-busy='true' role='progressbar'>
|
||||||
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 100 100' fill={APP_COLORS.bgPrimary}>
|
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 100 100' fill='currentColor'>
|
||||||
<path d='M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3 c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z'>
|
<path d='M31.6,3.5C5.9,13.6-6.6,42.7,3.5,68.4c10.1,25.7,39.2,38.3,64.9,28.1l-3.1-7.9c-21.3,8.4-45.4-2-53.8-23.3 c-8.4-21.3,2-45.4,23.3-53.8L31.6,3.5z'>
|
||||||
{animateRotation('2.25s')}
|
{animateRotation('2.25s')}
|
||||||
</path>
|
</path>
|
||||||
|
@ -73,8 +71,8 @@ export function Loader({ scale = 5, circular }: LoaderProps) {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center' aria-busy='true' role='progressbar'>
|
<div className='flex justify-center text-primary' aria-busy='true' role='progressbar'>
|
||||||
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 120 30' fill={APP_COLORS.bgPrimary}>
|
<svg height={`${scale * 20}`} width={`${scale * 20}`} viewBox='0 0 120 30' fill='currentColor'>
|
||||||
<circle cx='15' cy='15' r='16'>
|
<circle cx='15' cy='15' r='16'>
|
||||||
{animatePulse(true, '0.8s')}
|
{animatePulse(true, '0.8s')}
|
||||||
</circle>
|
</circle>
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function TabLabel({
|
||||||
titleHtml,
|
titleHtml,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
className,
|
className,
|
||||||
|
disabled,
|
||||||
role = 'tab',
|
role = 'tab',
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TabLabelProps) {
|
}: TabLabelProps) {
|
||||||
|
@ -28,10 +29,12 @@ export function TabLabel({
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'min-w-20 h-full',
|
'min-w-20 h-full',
|
||||||
'px-2 py-1 flex justify-center',
|
'px-2 py-1 flex justify-center',
|
||||||
'cc-hover cc-animate-color duration-150',
|
'cc-animate-color duration-select',
|
||||||
'text-sm whitespace-nowrap font-controls',
|
'text-sm whitespace-nowrap font-controls',
|
||||||
'select-none hover:cursor-pointer',
|
'select-none',
|
||||||
'outline-hidden',
|
'outline-hidden',
|
||||||
|
!disabled && 'hover:cursor-pointer cc-hover',
|
||||||
|
disabled && 'text-muted-foreground',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
tabIndex='-1'
|
tabIndex='-1'
|
||||||
|
@ -40,6 +43,7 @@ export function TabLabel({
|
||||||
data-tooltip-content={title}
|
data-tooltip-content={title}
|
||||||
data-tooltip-hidden={hideTitle}
|
data-tooltip-hidden={hideTitle}
|
||||||
role={role}
|
role={role}
|
||||||
|
disabled={disabled}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|
|
@ -61,4 +61,4 @@ export const authApi = {
|
||||||
endpoint: '/users/api/password-reset/confirm',
|
endpoint: '/users/api/password-reset/confirm',
|
||||||
request: { data: data }
|
request: { data: data }
|
||||||
})
|
})
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function ExpectedAnonymous() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cc-fade-in flex flex-col items-center gap-3 py-6'>
|
<div className='flex flex-col items-center gap-3 py-6'>
|
||||||
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
|
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
|
||||||
<div className='flex gap-3'>
|
<div className='flex gap-3'>
|
||||||
<TextURL text='Новая схема' href='/library/create' />
|
<TextURL text='Новая схема' href='/library/create' />
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function LoginPage() {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className='cc-column cc-fade-in w-96 mx-auto pt-12 pb-6 px-6'
|
className='cc-column w-96 mx-auto pt-12 pb-6 px-6'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
onChange={resetErrors}
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
@ -13,13 +13,26 @@ import { useQueryStrings } from '@/hooks/use-query-strings';
|
||||||
|
|
||||||
import { useResetPassword } from '../backend/use-reset-password';
|
import { useResetPassword } from '../backend/use-reset-password';
|
||||||
|
|
||||||
|
function useTokenValidation(token: string, isPending: boolean) {
|
||||||
|
const { validateToken } = useResetPassword();
|
||||||
|
const [isTokenValidating, setIsTokenValidating] = useState(false);
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
if (!isTokenValidating && !isPending) {
|
||||||
|
await validateToken({ token });
|
||||||
|
setIsTokenValidating(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { isTokenValidating, validate };
|
||||||
|
}
|
||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const token = useQueryStrings().get('token') ?? '';
|
const token = useQueryStrings().get('token') ?? '';
|
||||||
|
|
||||||
const { validateToken, resetPassword, isPending, error: serverError } = useResetPassword();
|
const { resetPassword, isPending, error: serverError } = useResetPassword();
|
||||||
|
const { isTokenValidating, validate } = useTokenValidation(token, isPending);
|
||||||
|
|
||||||
const [isTokenValidating, setIsTokenValidating] = useState(false);
|
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
|
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
|
||||||
|
|
||||||
|
@ -38,12 +51,9 @@ export function Component() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isTokenValidating && !isPending) {
|
if (!isTokenValidating && !isPending) {
|
||||||
void validateToken({ token: token });
|
void validate();
|
||||||
setIsTokenValidating(true);
|
|
||||||
}
|
}
|
||||||
}, [token, validateToken, isTokenValidating, isPending]);
|
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function Component() {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<form className='cc-fade-in cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
|
<form className='cc-column w-96 mx-auto px-6 mt-3' onSubmit={handleSubmit} onChange={clearServerError}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='email'
|
id='email'
|
||||||
autoComplete='email'
|
autoComplete='email'
|
||||||
|
|
|
@ -51,7 +51,7 @@ export function BadgeHelp({ topic, padding = 'p-1', className, contentClass, sty
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<Loader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className='absolute right-1 text-sm top-2 bg-input' onClick={event => event.stopPropagation()}>
|
<div className='absolute right-1 text-sm top-2 bg-' onClick={event => event.stopPropagation()}>
|
||||||
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
|
||||||
</div>
|
</div>
|
||||||
<TopicPage topic={topic} />
|
<TopicPage topic={topic} />
|
||||||
|
|
|
@ -3,12 +3,15 @@ import {
|
||||||
IconAnimation,
|
IconAnimation,
|
||||||
IconAnimationOff,
|
IconAnimationOff,
|
||||||
IconChild,
|
IconChild,
|
||||||
|
IconConceptBlock,
|
||||||
IconConnect,
|
IconConnect,
|
||||||
IconConsolidation,
|
IconConsolidation,
|
||||||
|
IconCoordinates,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
IconExecute,
|
IconExecute,
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
|
IconFixLayout,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconLineStraight,
|
IconLineStraight,
|
||||||
IconLineWave,
|
IconLineWave,
|
||||||
|
@ -16,7 +19,8 @@ import {
|
||||||
IconNewRSForm,
|
IconNewRSForm,
|
||||||
IconReset,
|
IconReset,
|
||||||
IconRSForm,
|
IconRSForm,
|
||||||
IconSave
|
IconSave,
|
||||||
|
IconSettings
|
||||||
} from '@/components/icons';
|
} from '@/components/icons';
|
||||||
|
|
||||||
import { LinkTopic } from '../../components/link-topic';
|
import { LinkTopic } from '../../components/link-topic';
|
||||||
|
@ -29,9 +33,19 @@ export function HelpOssGraph() {
|
||||||
<div className='flex flex-col sm:flex-row'>
|
<div className='flex flex-col sm:flex-row'>
|
||||||
<div className='sm:w-56'>
|
<div className='sm:w-56'>
|
||||||
<h1>Настройка графа</h1>
|
<h1>Настройка графа</h1>
|
||||||
|
<li>
|
||||||
|
<IconReset className='inline-icon' /> Сбросить изменения
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconFixLayout className='inline-icon' /> Исправить расположения
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconSettings className='inline-icon' /> Диалог настроек
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<IconGrid className='inline-icon' /> Отображение сетки
|
<IconGrid className='inline-icon' /> Отображение сетки
|
||||||
</li>
|
</li>
|
||||||
|
@ -43,6 +57,9 @@ export function HelpOssGraph() {
|
||||||
<IconAnimation className='inline-icon' />
|
<IconAnimation className='inline-icon' />
|
||||||
<IconAnimationOff className='inline-icon' /> Анимация
|
<IconAnimationOff className='inline-icon' /> Анимация
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconCoordinates className='inline-icon' /> Отображение координат
|
||||||
|
</li>
|
||||||
<li>черта сверху - Загрузка</li>
|
<li>черта сверху - Загрузка</li>
|
||||||
<li>
|
<li>
|
||||||
черта слева - КС <LinkTopic text='внешняя' topic={HelpTopic.CC_OSS} />
|
черта слева - КС <LinkTopic text='внешняя' topic={HelpTopic.CC_OSS} />
|
||||||
|
@ -63,11 +80,14 @@ export function HelpOssGraph() {
|
||||||
<kbd>Двойной клик</kbd> – переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
|
<kbd>Двойной клик</kbd> – переход к связанной <LinkTopic text='КС' topic={HelpTopic.CC_SYSTEM} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconEdit2 className='inline-icon' /> Редактирование операции
|
<IconConceptBlock className='inline-icon icon-green' /> Новый блок
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconNewItem className='inline-icon icon-green' /> Новая операция
|
<IconNewItem className='inline-icon icon-green' /> Новая операция
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconEdit2 className='inline-icon' /> Редактирование узла
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> – удалить выбранные
|
<IconDestroy className='inline-icon icon-red' /> <kbd>Delete</kbd> – удалить выбранные
|
||||||
</li>
|
</li>
|
||||||
|
@ -80,10 +100,13 @@ export function HelpOssGraph() {
|
||||||
<div className='sm:w-56'>
|
<div className='sm:w-56'>
|
||||||
<h1>Общие</h1>
|
<h1>Общие</h1>
|
||||||
<li>
|
<li>
|
||||||
<IconReset className='inline-icon' /> Сбросить изменения
|
<IconSave className='inline-icon' /> Сохранить положения
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconSave className='inline-icon' /> Сохранить положения
|
<kbd>Space</kbd> – перемещение экрана
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Shift</kbd> – перемещение выделенных элементов в границах родителя
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -71,12 +71,12 @@ export function HelpRSEditor() {
|
||||||
<span className='bg-selected'>текущая конституента</span>
|
<span className='bg-selected'>текущая конституента</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='bg-(--acc-bg-green50)'>
|
<span className='bg-accent-green50'>
|
||||||
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
<LinkTopic text='основа' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='bg-(--acc-bg-orange50)'>
|
<span className='bg-accent-orange50'>
|
||||||
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
<LinkTopic text='порожденные' topic={HelpTopic.CC_RELATIONS} /> текущей
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -10,7 +10,9 @@ import {
|
||||||
IconEditor,
|
IconEditor,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
|
IconQR,
|
||||||
IconReader,
|
IconReader,
|
||||||
|
IconRobot,
|
||||||
IconShare,
|
IconShare,
|
||||||
IconUpload
|
IconUpload
|
||||||
} from '@/components/icons';
|
} from '@/components/icons';
|
||||||
|
@ -51,6 +53,12 @@ export function HelpRSMenu() {
|
||||||
<li>
|
<li>
|
||||||
<IconShare className='inline-icon' /> Поделиться – скопировать ссылку на схему
|
<IconShare className='inline-icon' /> Поделиться – скопировать ссылку на схему
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconQR className='inline-icon' /> Отобразить QR-код схемы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IconRobot className='inline-icon' /> Генерировать запрос для LLM
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -19,13 +19,13 @@ export function HelpTypeGraph() {
|
||||||
|
|
||||||
<h2>Виды узлов</h2>
|
<h2>Виды узлов</h2>
|
||||||
<li>
|
<li>
|
||||||
<span className='bg-prim-200'>ступень-основание</span>
|
<span className='bg-secondary'>ступень-основание</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='bg-(--acc-bg-teal)'>ступень-булеан</span>
|
<span className='bg-accent-teal'>ступень-булеан</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className='bg-(--acc-bg-orange)'>ступень декартова произведения</span>
|
<span className='bg-accent-orange'>ступень декартова произведения</span>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,7 +57,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
getLabel={labelHelpTopic}
|
getLabel={labelHelpTopic}
|
||||||
getDescription={describeHelpTopic}
|
getDescription={describeHelpTopic}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-prim-200',
|
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary',
|
||||||
menu.isOpen && 'open'
|
menu.isOpen && 'open'
|
||||||
)}
|
)}
|
||||||
style={{ maxHeight: treeHeight }}
|
style={{ maxHeight: treeHeight }}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps)
|
||||||
'cc-scroll-y',
|
'cc-scroll-y',
|
||||||
'self-start',
|
'self-start',
|
||||||
'border-x border-t rounded-none',
|
'border-x border-t rounded-none',
|
||||||
'text-xs sm:text-sm bg-prim-200',
|
'text-xs sm:text-sm bg-secondary',
|
||||||
'select-none'
|
'select-none'
|
||||||
)}
|
)}
|
||||||
style={{ maxHeight: topicsHeight }}
|
style={{ maxHeight: topicsHeight }}
|
||||||
|
|
|
@ -13,11 +13,7 @@ interface ViewTopicProps {
|
||||||
export function ViewTopic({ topic }: ViewTopicProps) {
|
export function ViewTopic({ topic }: ViewTopicProps) {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={topic} className='py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto' style={{ maxHeight: mainHeight }}>
|
||||||
key={topic}
|
|
||||||
className='cc-fade-in py-2 px-6 mx-auto sm:mx-0 lg:px-12 overflow-y-auto'
|
|
||||||
style={{ maxHeight: mainHeight }}
|
|
||||||
>
|
|
||||||
<TopicPage topic={topic} />
|
<TopicPage topic={topic} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function Component() {
|
||||||
}, [hideFooter]);
|
}, [hideFooter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
|
<div className='flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
|
||||||
<TransformWrapper>
|
<TransformWrapper>
|
||||||
<TransformComponent>
|
<TransformComponent>
|
||||||
<img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' />
|
<img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' />
|
||||||
|
|
|
@ -15,12 +15,12 @@ import {
|
||||||
type AccessPolicy,
|
type AccessPolicy,
|
||||||
type ICloneLibraryItemDTO,
|
type ICloneLibraryItemDTO,
|
||||||
type ICreateLibraryItemDTO,
|
type ICreateLibraryItemDTO,
|
||||||
|
type ICreateVersionDTO,
|
||||||
type ILibraryItem,
|
type ILibraryItem,
|
||||||
type IRenameLocationDTO,
|
type IRenameLocationDTO,
|
||||||
type IUpdateLibraryItemDTO,
|
type IUpdateLibraryItemDTO,
|
||||||
type IVersionCreateDTO,
|
type IUpdateVersionDTO,
|
||||||
type IVersionExInfo,
|
type IVersionExInfo,
|
||||||
type IVersionUpdateDTO,
|
|
||||||
schemaLibraryItem,
|
schemaLibraryItem,
|
||||||
schemaLibraryItemArray,
|
schemaLibraryItemArray,
|
||||||
schemaVersionExInfo
|
schemaVersionExInfo
|
||||||
|
@ -136,8 +136,8 @@ export const libraryApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
versionCreate: ({ itemID, data }: { itemID: number; data: IVersionCreateDTO }) =>
|
createVersion: ({ itemID, data }: { itemID: number; data: ICreateVersionDTO }) =>
|
||||||
axiosPost<IVersionCreateDTO, IVersionCreatedResponse>({
|
axiosPost<ICreateVersionDTO, IVersionCreatedResponse>({
|
||||||
schema: schemaVersionCreatedResponse,
|
schema: schemaVersionCreatedResponse,
|
||||||
endpoint: `/api/library/${itemID}/create-version`,
|
endpoint: `/api/library/${itemID}/create-version`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -145,7 +145,7 @@ export const libraryApi = {
|
||||||
successMessage: infoMsg.newVersion(data.version)
|
successMessage: infoMsg.newVersion(data.version)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
versionRestore: ({ versionID }: { versionID: number }) =>
|
restoreVersion: ({ versionID }: { versionID: number }) =>
|
||||||
axiosPatch<undefined, IRSFormDTO>({
|
axiosPatch<undefined, IRSFormDTO>({
|
||||||
schema: schemaRSForm,
|
schema: schemaRSForm,
|
||||||
endpoint: `/api/versions/${versionID}/restore`,
|
endpoint: `/api/versions/${versionID}/restore`,
|
||||||
|
@ -153,8 +153,8 @@ export const libraryApi = {
|
||||||
successMessage: infoMsg.versionRestored
|
successMessage: infoMsg.versionRestored
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) =>
|
updateVersion: (data: { itemID: number; version: IUpdateVersionDTO }) =>
|
||||||
axiosPatch<IVersionUpdateDTO, IVersionExInfo>({
|
axiosPatch<IUpdateVersionDTO, IVersionExInfo>({
|
||||||
schema: schemaVersionExInfo,
|
schema: schemaVersionExInfo,
|
||||||
endpoint: `/api/versions/${data.version.id}`,
|
endpoint: `/api/versions/${data.version.id}`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -162,11 +162,11 @@ export const libraryApi = {
|
||||||
successMessage: infoMsg.changesSaved
|
successMessage: infoMsg.changesSaved
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
versionDelete: (data: { itemID: number; versionID: number }) =>
|
deleteVersion: (data: { itemID: number; versionID: number }) =>
|
||||||
axiosDelete({
|
axiosDelete({
|
||||||
endpoint: `/api/versions/${data.versionID}`,
|
endpoint: `/api/versions/${data.versionID}`,
|
||||||
request: {
|
request: {
|
||||||
successMessage: infoMsg.versionDestroyed
|
successMessage: infoMsg.versionDestroyed
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -49,10 +49,10 @@ export type ICreateLibraryItemDTO = z.infer<typeof schemaCreateLibraryItem>;
|
||||||
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
|
export type IUpdateLibraryItemDTO = z.infer<typeof schemaUpdateLibraryItem>;
|
||||||
|
|
||||||
/** Create version metadata in persistent storage. */
|
/** Create version metadata in persistent storage. */
|
||||||
export type IVersionCreateDTO = z.infer<typeof schemaVersionCreate>;
|
export type ICreateVersionDTO = z.infer<typeof schemaCreateVersion>;
|
||||||
|
|
||||||
/** Represents version data, intended to update version metadata in persistent storage. */
|
/** Represents version data, intended to update version metadata in persistent storage. */
|
||||||
export type IVersionUpdateDTO = z.infer<typeof schemaVersionUpdate>;
|
export type IUpdateVersionDTO = z.infer<typeof schemaUpdateVersion>;
|
||||||
|
|
||||||
// ======= SCHEMAS =========
|
// ======= SCHEMAS =========
|
||||||
export const schemaLibraryItemType = z.enum(Object.values(LibraryItemType) as [LibraryItemType, ...LibraryItemType[]]);
|
export const schemaLibraryItemType = z.enum(Object.values(LibraryItemType) as [LibraryItemType, ...LibraryItemType[]]);
|
||||||
|
@ -140,13 +140,13 @@ export const schemaVersionExInfo = schemaVersionInfo.extend({
|
||||||
item: z.number()
|
item: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaVersionUpdate = z.strictObject({
|
export const schemaUpdateVersion = z.strictObject({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
version: z.string().nonempty(errorMsg.requiredField),
|
version: z.string().nonempty(errorMsg.requiredField),
|
||||||
description: z.string()
|
description: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaVersionCreate = z.strictObject({
|
export const schemaCreateVersion = z.strictObject({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
items: z.array(z.number())
|
items: z.array(z.number())
|
||||||
|
|
|
@ -3,15 +3,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type IVersionCreateDTO } from './types';
|
import { type ICreateVersionDTO } from './types';
|
||||||
import { useUpdateTimestamp } from './use-update-timestamp';
|
import { useUpdateTimestamp } from './use-update-timestamp';
|
||||||
|
|
||||||
export const useVersionCreate = () => {
|
export const useCreateVersion = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const { updateTimestamp } = useUpdateTimestamp();
|
const { updateTimestamp } = useUpdateTimestamp();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'create-version'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'create-version'],
|
||||||
mutationFn: libraryApi.versionCreate,
|
mutationFn: libraryApi.createVersion,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
|
client.setQueryData(KEYS.composite.rsItem({ itemID: data.schema.id }), data.schema);
|
||||||
updateTimestamp(data.schema.id);
|
updateTimestamp(data.schema.id);
|
||||||
|
@ -19,7 +19,7 @@ export const useVersionCreate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
versionCreate: (data: { itemID: number; data: IVersionCreateDTO }) =>
|
createVersion: (data: { itemID: number; data: ICreateVersionDTO }) =>
|
||||||
mutation.mutateAsync(data).then(response => response.version)
|
mutation.mutateAsync(data).then(response => response.version)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -6,11 +6,11 @@ import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
export const useVersionDelete = () => {
|
export const useDeleteVersion = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-version'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-version'],
|
||||||
mutationFn: libraryApi.versionDelete,
|
mutationFn: libraryApi.deleteVersion,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
||||||
!prev
|
!prev
|
||||||
|
@ -24,6 +24,6 @@ export const useVersionDelete = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
versionDelete: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
|
deleteVersion: (data: { itemID: number; versionID: number }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -7,6 +7,11 @@ import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
|
export function useLibraryListKey() {
|
||||||
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
return libraryApi.getLibraryQueryOptions({ isAdmin: adminMode }).queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
export function useLibrarySuspense() {
|
export function useLibrarySuspense() {
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
const { user } = useAuthSuspense();
|
const { user } = useAuthSuspense();
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
export const useVersionRestore = () => {
|
export const useRestoreVersion = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'restore-version'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'restore-version'],
|
||||||
mutationFn: libraryApi.versionRestore,
|
mutationFn: libraryApi.restoreVersion,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
|
client.setQueryData(KEYS.composite.rsItem({ itemID: data.id }), data);
|
||||||
return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
|
return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
|
||||||
|
@ -16,6 +16,6 @@ export const useVersionRestore = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
versionRestore: (data: { versionID: number }) => mutation.mutateAsync(data)
|
restoreVersion: (data: { versionID: number }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -4,12 +4,15 @@ import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type AccessPolicy, type ILibraryItem } from './types';
|
import { type AccessPolicy, type ILibraryItem } from './types';
|
||||||
|
import { useLibraryListKey } from './use-library';
|
||||||
|
|
||||||
export const useSetAccessPolicy = () => {
|
export const useSetAccessPolicy = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
const libraryKey = useLibraryListKey();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
|
||||||
mutationFn: libraryApi.setAccessPolicy,
|
mutationFn: libraryApi.setAccessPolicy,
|
||||||
|
@ -36,7 +39,7 @@ export const useSetAccessPolicy = () => {
|
||||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||||
!prev ? undefined : { ...prev, access_policy: variables.policy }
|
!prev ? undefined : { ...prev, access_policy: variables.policy }
|
||||||
);
|
);
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
|
||||||
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
|
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,12 +4,15 @@ import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type ILibraryItem } from './types';
|
import { type ILibraryItem } from './types';
|
||||||
|
import { useLibraryListKey } from './use-library';
|
||||||
|
|
||||||
export const useSetLocation = () => {
|
export const useSetLocation = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
const libraryKey = useLibraryListKey();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-location'],
|
||||||
mutationFn: libraryApi.setLocation,
|
mutationFn: libraryApi.setLocation,
|
||||||
|
@ -36,7 +39,7 @@ export const useSetLocation = () => {
|
||||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||||
!prev ? undefined : { ...prev, location: variables.location }
|
!prev ? undefined : { ...prev, location: variables.location }
|
||||||
);
|
);
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
|
||||||
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
|
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,12 +4,15 @@ import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type ILibraryItem } from './types';
|
import { type ILibraryItem } from './types';
|
||||||
|
import { useLibraryListKey } from './use-library';
|
||||||
|
|
||||||
export const useSetOwner = () => {
|
export const useSetOwner = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
const libraryKey = useLibraryListKey();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-owner'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'set-owner'],
|
||||||
mutationFn: libraryApi.setOwner,
|
mutationFn: libraryApi.setOwner,
|
||||||
|
@ -36,7 +39,7 @@ export const useSetOwner = () => {
|
||||||
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
client.setQueryData(rsKey, (prev: IRSFormDTO | undefined) =>
|
||||||
!prev ? undefined : { ...prev, owner: variables.owner }
|
!prev ? undefined : { ...prev, owner: variables.owner }
|
||||||
);
|
);
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
|
||||||
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
|
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,12 +4,15 @@ import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type ILibraryItem, type IUpdateLibraryItemDTO, LibraryItemType } from './types';
|
import { type ILibraryItem, type IUpdateLibraryItemDTO, LibraryItemType } from './types';
|
||||||
|
import { useLibraryListKey } from './use-library';
|
||||||
|
|
||||||
export const useUpdateItem = () => {
|
export const useUpdateItem = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
const libraryKey = useLibraryListKey();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-item'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-item'],
|
||||||
mutationFn: libraryApi.updateItem,
|
mutationFn: libraryApi.updateItem,
|
||||||
|
@ -18,7 +21,7 @@ export const useUpdateItem = () => {
|
||||||
data.item_type === LibraryItemType.RSFORM
|
data.item_type === LibraryItemType.RSFORM
|
||||||
? KEYS.composite.rsItem({ itemID: data.id })
|
? KEYS.composite.rsItem({ itemID: data.id })
|
||||||
: KEYS.composite.ossItem({ itemID: data.id });
|
: KEYS.composite.ossItem({ itemID: data.id });
|
||||||
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
|
client.setQueryData(libraryKey, (prev: RO<ILibraryItem[]> | undefined) =>
|
||||||
prev?.map(item => (item.id === data.id ? data : item))
|
prev?.map(item => (item.id === data.id ? data : item))
|
||||||
);
|
);
|
||||||
client.setQueryData(itemKey, (prev: IRSFormDTO | IOperationSchemaDTO | undefined) =>
|
client.setQueryData(itemKey, (prev: IRSFormDTO | IOperationSchemaDTO | undefined) =>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { type ILibraryItem } from './types';
|
import { type ILibraryItem } from './types';
|
||||||
|
import { useLibraryListKey } from './use-library';
|
||||||
|
|
||||||
export function useUpdateTimestamp() {
|
export function useUpdateTimestamp() {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
const libraryKey = useLibraryListKey();
|
||||||
return {
|
return {
|
||||||
updateTimestamp: (target: number) =>
|
updateTimestamp: (target: number) =>
|
||||||
client.setQueryData(
|
client.setQueryData(
|
||||||
libraryApi.libraryListKey, //
|
libraryKey, //
|
||||||
(prev: ILibraryItem[] | undefined) =>
|
(prev: RO<ILibraryItem[]> | undefined) =>
|
||||||
prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item))
|
prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item))
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { type IRSFormDTO } from '@/features/rsform';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
import { type IVersionUpdateDTO } from './types';
|
import { type IUpdateVersionDTO } from './types';
|
||||||
|
|
||||||
export const useVersionUpdate = () => {
|
export const useUpdateVersion = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-version'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'update-version'],
|
||||||
mutationFn: libraryApi.versionUpdate,
|
mutationFn: libraryApi.updateVersion,
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
client.setQueryData(KEYS.composite.rsItem({ itemID: variables.itemID }), (prev: IRSFormDTO | undefined) =>
|
||||||
!prev
|
!prev
|
||||||
|
@ -41,6 +41,6 @@ export const useVersionUpdate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
versionUpdate: (data: { itemID: number; version: IVersionUpdateDTO }) => mutation.mutateAsync(data)
|
updateVersion: (data: { itemID: number; version: IUpdateVersionDTO }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -8,7 +8,6 @@ import { IconClose, IconFolderTree } from '@/components/icons';
|
||||||
import { SearchBar } from '@/components/input';
|
import { SearchBar } from '@/components/input';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { cn } from '@/components/utils';
|
import { cn } from '@/components/utils';
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { type ILibraryItem, type LibraryItemType } from '../backend/types';
|
import { type ILibraryItem, type LibraryItemType } from '../backend/types';
|
||||||
|
@ -91,7 +90,7 @@ export function PickSchema({
|
||||||
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
||||||
{
|
{
|
||||||
when: (item: ILibraryItem) => item.id === value,
|
when: (item: ILibraryItem) => item.id === value,
|
||||||
style: { backgroundColor: APP_COLORS.bgSelected }
|
className: 'bg-selected'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
!dense && 'h-7 sm:h-8',
|
!dense && 'h-7 sm:h-8',
|
||||||
'pr-3 py-1 flex items-center gap-2',
|
'pr-3 py-1 flex items-center gap-2',
|
||||||
'cc-scroll-row',
|
'cc-scroll-row',
|
||||||
'cc-hover cc-animate-color',
|
'cc-hover cc-animate-color duration-fade',
|
||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
'leading-3 sm:leading-4',
|
'leading-3 sm:leading-4',
|
||||||
activeNode === item && 'cc-selected'
|
activeNode === item && 'cc-selected'
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { ModalForm } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { type IVersionCreateDTO, type IVersionInfo, schemaVersionCreate } from '../backend/types';
|
import { type ICreateVersionDTO, type IVersionInfo, schemaCreateVersion } from '../backend/types';
|
||||||
import { useVersionCreate } from '../backend/use-version-create';
|
import { useCreateVersion } from '../backend/use-create-version';
|
||||||
import { nextVersion } from '../models/library-api';
|
import { nextVersion } from '../models/library-api';
|
||||||
|
|
||||||
export interface DlgCreateVersionProps {
|
export interface DlgCreateVersionProps {
|
||||||
|
@ -24,10 +24,10 @@ export function DlgCreateVersion() {
|
||||||
const { itemID, versions, selected, totalCount, onCreate } = useDialogsStore(
|
const { itemID, versions, selected, totalCount, onCreate } = useDialogsStore(
|
||||||
state => state.props as DlgCreateVersionProps
|
state => state.props as DlgCreateVersionProps
|
||||||
);
|
);
|
||||||
const { versionCreate } = useVersionCreate();
|
const { createVersion: versionCreate } = useCreateVersion();
|
||||||
|
|
||||||
const { register, handleSubmit, control } = useForm<IVersionCreateDTO>({
|
const { register, handleSubmit, control } = useForm<ICreateVersionDTO>({
|
||||||
resolver: zodResolver(schemaVersionCreate),
|
resolver: zodResolver(schemaCreateVersion),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
|
version: versions.length > 0 ? nextVersion(versions[versions.length - 1].version) : '1.0.0',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -37,7 +37,7 @@ export function DlgCreateVersion() {
|
||||||
const version = useWatch({ control, name: 'version' });
|
const version = useWatch({ control, name: 'version' });
|
||||||
const canSubmit = !versions.find(ver => ver.version === version);
|
const canSubmit = !versions.find(ver => ver.version === version);
|
||||||
|
|
||||||
function onSubmit(data: IVersionCreateDTO) {
|
function onSubmit(data: ICreateVersionDTO) {
|
||||||
return versionCreate({ itemID, data }).then(onCreate);
|
return versionCreate({ itemID, data }).then(onCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { ModalView } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { type IVersionUpdateDTO, schemaVersionUpdate } from '../../backend/types';
|
import { type IUpdateVersionDTO, schemaUpdateVersion } from '../../backend/types';
|
||||||
|
import { useDeleteVersion } from '../../backend/use-delete-version';
|
||||||
import { useMutatingLibrary } from '../../backend/use-mutating-library';
|
import { useMutatingLibrary } from '../../backend/use-mutating-library';
|
||||||
import { useVersionDelete } from '../../backend/use-version-delete';
|
import { useUpdateVersion } from '../../backend/use-update-version';
|
||||||
import { useVersionUpdate } from '../../backend/use-version-update';
|
|
||||||
|
|
||||||
import { TableVersions } from './table-versions';
|
import { TableVersions } from './table-versions';
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ export function DlgEditVersions() {
|
||||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||||
const { schema } = useRSFormSuspense({ itemID });
|
const { schema } = useRSFormSuspense({ itemID });
|
||||||
const isProcessing = useMutatingLibrary();
|
const isProcessing = useMutatingLibrary();
|
||||||
const { versionDelete } = useVersionDelete();
|
const { deleteVersion: versionDelete } = useDeleteVersion();
|
||||||
const { versionUpdate } = useVersionUpdate();
|
const { updateVersion: versionUpdate } = useUpdateVersion();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
@ -40,8 +40,8 @@ export function DlgEditVersions() {
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
formState: { isDirty, errors: formErrors }
|
formState: { isDirty, errors: formErrors }
|
||||||
} = useForm<IVersionUpdateDTO>({
|
} = useForm<IUpdateVersionDTO>({
|
||||||
resolver: zodResolver(schemaVersionUpdate),
|
resolver: zodResolver(schemaUpdateVersion),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: schema.versions[schema.versions.length - 1].id,
|
id: schema.versions[schema.versions.length - 1].id,
|
||||||
version: schema.versions[schema.versions.length - 1].version,
|
version: schema.versions[schema.versions.length - 1].version,
|
||||||
|
@ -77,7 +77,7 @@ export function DlgEditVersions() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdate(data: IVersionUpdateDTO) {
|
function onUpdate(data: IUpdateVersionDTO) {
|
||||||
if (!isDirty || isProcessing || !isValid) {
|
if (!isDirty || isProcessing || !isValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { useIntl } from 'react-intl';
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
||||||
import { IconRemove } from '@/components/icons';
|
import { IconRemove } from '@/components/icons';
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
|
||||||
|
|
||||||
import { type IVersionInfo } from '../../backend/types';
|
import { type IVersionInfo } from '../../backend/types';
|
||||||
|
|
||||||
|
@ -77,9 +76,7 @@ export function TableVersions({ processing, items, onDelete, selected, onSelect
|
||||||
const conditionalRowStyles: IConditionalStyle<IVersionInfo>[] = [
|
const conditionalRowStyles: IConditionalStyle<IVersionInfo>[] = [
|
||||||
{
|
{
|
||||||
when: (version: IVersionInfo) => version.id === selected,
|
when: (version: IVersionInfo) => version.id === selected,
|
||||||
style: {
|
className: 'bg-selected'
|
||||||
backgroundColor: APP_COLORS.bgSelected
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ export function FormCreateItem() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className='cc-fade-in cc-column w-120 mx-auto px-6 py-3'
|
className='cc-column w-120 mx-auto px-6 py-3'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
onChange={resetErrors}
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function LibraryPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
||||||
<div className='relative cc-fade-in flex'>
|
<div className='relative flex'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выгрузить в формате CSV'
|
title='Выгрузить в формате CSV'
|
||||||
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { DataTable, type IConditionalStyle, type VisibilityState } from '@/compo
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { useFitHeight } from '@/stores/app-layout';
|
import { useFitHeight } from '@/stores/app-layout';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
|
import { type ILibraryItem, LibraryItemType } from '../../backend/types';
|
||||||
import { useLibrarySearchStore } from '../../stores/library-search';
|
import { useLibrarySearchStore } from '../../stores/library-search';
|
||||||
|
@ -17,7 +17,7 @@ import { useLibrarySearchStore } from '../../stores/library-search';
|
||||||
import { useLibraryColumns } from './use-library-columns';
|
import { useLibraryColumns } from './use-library-columns';
|
||||||
|
|
||||||
interface TableLibraryItemsProps {
|
interface TableLibraryItemsProps {
|
||||||
items: ILibraryItem[];
|
items: RO<ILibraryItem[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
|
@ -35,9 +35,7 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
|
||||||
{
|
{
|
||||||
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
|
when: (item: ILibraryItem) => item.item_type === LibraryItemType.OSS,
|
||||||
style: {
|
className: 'text-accent-green-foreground'
|
||||||
color: APP_COLORS.fgGreen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const tableHeight = useFitHeight('2.25rem');
|
const tableHeight = useFitHeight('2.25rem');
|
||||||
|
@ -58,7 +56,7 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
<DataTable
|
<DataTable
|
||||||
id='library_data'
|
id='library_data'
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items as ILibraryItem[]}
|
||||||
headPosition='0'
|
headPosition='0'
|
||||||
className={clsx('cc-scroll-y h-fit text-xs sm:text-sm border-b', folderMode && 'border-l')}
|
className={clsx('cc-scroll-y h-fit text-xs sm:text-sm border-b', folderMode && 'border-l')}
|
||||||
style={{ maxHeight: tableHeight }}
|
style={{ maxHeight: tableHeight }}
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex h-full grow pr-4'>
|
<div className='flex h-full grow pr-4 sm:pr-12'>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
id='library_search'
|
id='library_search'
|
||||||
placeholder='Поиск'
|
placeholder='Поиск'
|
||||||
|
|
|
@ -6,12 +6,13 @@ import { MiniButton } from '@/components/control';
|
||||||
import { createColumnHelper } from '@/components/data-table';
|
import { createColumnHelper } from '@/components/data-table';
|
||||||
import { IconFolderTree } from '@/components/icons';
|
import { IconFolderTree } from '@/components/icons';
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { type ILibraryItem } from '../../backend/types';
|
import { type ILibraryItem } from '../../backend/types';
|
||||||
import { BadgeLocation } from '../../components/badge-location';
|
import { BadgeLocation } from '../../components/badge-location';
|
||||||
import { useLibrarySearchStore } from '../../stores/library-search';
|
import { useLibrarySearchStore } from '../../stores/library-search';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
const columnHelper = createColumnHelper<RO<ILibraryItem>>();
|
||||||
|
|
||||||
export function useLibraryColumns() {
|
export function useLibraryColumns() {
|
||||||
const { isSmall } = useWindowSize();
|
const { isSmall } = useWindowSize();
|
||||||
|
|
|
@ -5,17 +5,23 @@ import { DELAYS, KEYS } from '@/backend/configuration';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IBlockCreatedResponse,
|
||||||
type IConstituentaReference,
|
type IConstituentaReference,
|
||||||
type ICstRelocateDTO,
|
type ICreateBlockDTO,
|
||||||
|
type ICreateOperationDTO,
|
||||||
|
type IDeleteBlockDTO,
|
||||||
|
type IDeleteOperationDTO,
|
||||||
type IInputCreatedResponse,
|
type IInputCreatedResponse,
|
||||||
type IInputUpdateDTO,
|
type IMoveItemsDTO,
|
||||||
type IOperationCreatedResponse,
|
type IOperationCreatedResponse,
|
||||||
type IOperationCreateDTO,
|
|
||||||
type IOperationDeleteDTO,
|
|
||||||
type IOperationSchemaDTO,
|
type IOperationSchemaDTO,
|
||||||
type IOperationUpdateDTO,
|
|
||||||
type IOssLayout,
|
type IOssLayout,
|
||||||
|
type IRelocateConstituentsDTO,
|
||||||
type ITargetOperation,
|
type ITargetOperation,
|
||||||
|
type IUpdateBlockDTO,
|
||||||
|
type IUpdateInputDTO,
|
||||||
|
type IUpdateOperationDTO,
|
||||||
|
schemaBlockCreatedResponse,
|
||||||
schemaConstituentaReference,
|
schemaConstituentaReference,
|
||||||
schemaInputCreatedResponse,
|
schemaInputCreatedResponse,
|
||||||
schemaOperationCreatedResponse,
|
schemaOperationCreatedResponse,
|
||||||
|
@ -49,8 +55,36 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
operationCreate: ({ itemID, data }: { itemID: number; data: IOperationCreateDTO }) =>
|
createBlock: ({ itemID, data }: { itemID: number; data: ICreateBlockDTO }) =>
|
||||||
axiosPost<IOperationCreateDTO, IOperationCreatedResponse>({
|
axiosPost<ICreateBlockDTO, IBlockCreatedResponse>({
|
||||||
|
schema: schemaBlockCreatedResponse,
|
||||||
|
endpoint: `/api/oss/${itemID}/create-block`,
|
||||||
|
request: {
|
||||||
|
data: data,
|
||||||
|
successMessage: infoMsg.changesSaved
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateBlock: ({ itemID, data }: { itemID: number; data: IUpdateBlockDTO }) =>
|
||||||
|
axiosPatch<IUpdateBlockDTO, IOperationSchemaDTO>({
|
||||||
|
schema: schemaOperationSchema,
|
||||||
|
endpoint: `/api/oss/${itemID}/update-block`,
|
||||||
|
request: {
|
||||||
|
data: data,
|
||||||
|
successMessage: infoMsg.changesSaved
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteBlock: ({ itemID, data }: { itemID: number; data: IDeleteBlockDTO }) =>
|
||||||
|
axiosPatch<IDeleteBlockDTO, IOperationSchemaDTO>({
|
||||||
|
schema: schemaOperationSchema,
|
||||||
|
endpoint: `/api/oss/${itemID}/delete-block`,
|
||||||
|
request: {
|
||||||
|
data: data,
|
||||||
|
successMessage: infoMsg.blockDestroyed
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
createOperation: ({ itemID, data }: { itemID: number; data: ICreateOperationDTO }) =>
|
||||||
|
axiosPost<ICreateOperationDTO, IOperationCreatedResponse>({
|
||||||
schema: schemaOperationCreatedResponse,
|
schema: schemaOperationCreatedResponse,
|
||||||
endpoint: `/api/oss/${itemID}/create-operation`,
|
endpoint: `/api/oss/${itemID}/create-operation`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -58,8 +92,17 @@ export const ossApi = {
|
||||||
successMessage: response => infoMsg.newOperation(response.new_operation.alias)
|
successMessage: response => infoMsg.newOperation(response.new_operation.alias)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
operationDelete: ({ itemID, data }: { itemID: number; data: IOperationDeleteDTO }) =>
|
updateOperation: ({ itemID, data }: { itemID: number; data: IUpdateOperationDTO }) =>
|
||||||
axiosPatch<IOperationDeleteDTO, IOperationSchemaDTO>({
|
axiosPatch<IUpdateOperationDTO, IOperationSchemaDTO>({
|
||||||
|
schema: schemaOperationSchema,
|
||||||
|
endpoint: `/api/oss/${itemID}/update-operation`,
|
||||||
|
request: {
|
||||||
|
data: data,
|
||||||
|
successMessage: infoMsg.changesSaved
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteOperation: ({ itemID, data }: { itemID: number; data: IDeleteOperationDTO }) =>
|
||||||
|
axiosPatch<IDeleteOperationDTO, IOperationSchemaDTO>({
|
||||||
schema: schemaOperationSchema,
|
schema: schemaOperationSchema,
|
||||||
endpoint: `/api/oss/${itemID}/delete-operation`,
|
endpoint: `/api/oss/${itemID}/delete-operation`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -67,7 +110,8 @@ export const ossApi = {
|
||||||
successMessage: infoMsg.operationDestroyed
|
successMessage: infoMsg.operationDestroyed
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
inputCreate: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
|
|
||||||
|
createInput: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
|
||||||
axiosPatch<ITargetOperation, IInputCreatedResponse>({
|
axiosPatch<ITargetOperation, IInputCreatedResponse>({
|
||||||
schema: schemaInputCreatedResponse,
|
schema: schemaInputCreatedResponse,
|
||||||
endpoint: `/api/oss/${itemID}/create-input`,
|
endpoint: `/api/oss/${itemID}/create-input`,
|
||||||
|
@ -76,8 +120,8 @@ export const ossApi = {
|
||||||
successMessage: infoMsg.newLibraryItem
|
successMessage: infoMsg.newLibraryItem
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
inputUpdate: ({ itemID, data }: { itemID: number; data: IInputUpdateDTO }) =>
|
updateInput: ({ itemID, data }: { itemID: number; data: IUpdateInputDTO }) =>
|
||||||
axiosPatch<IInputUpdateDTO, IOperationSchemaDTO>({
|
axiosPatch<IUpdateInputDTO, IOperationSchemaDTO>({
|
||||||
schema: schemaOperationSchema,
|
schema: schemaOperationSchema,
|
||||||
endpoint: `/api/oss/${itemID}/set-input`,
|
endpoint: `/api/oss/${itemID}/set-input`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -85,16 +129,7 @@ export const ossApi = {
|
||||||
successMessage: infoMsg.changesSaved
|
successMessage: infoMsg.changesSaved
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
operationUpdate: ({ itemID, data }: { itemID: number; data: IOperationUpdateDTO }) =>
|
executeOperation: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
|
||||||
axiosPatch<IOperationUpdateDTO, IOperationSchemaDTO>({
|
|
||||||
schema: schemaOperationSchema,
|
|
||||||
endpoint: `/api/oss/${itemID}/update-operation`,
|
|
||||||
request: {
|
|
||||||
data: data,
|
|
||||||
successMessage: infoMsg.changesSaved
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
operationExecute: ({ itemID, data }: { itemID: number; data: ITargetOperation }) =>
|
|
||||||
axiosPost<ITargetOperation, IOperationSchemaDTO>({
|
axiosPost<ITargetOperation, IOperationSchemaDTO>({
|
||||||
schema: schemaOperationSchema,
|
schema: schemaOperationSchema,
|
||||||
endpoint: `/api/oss/${itemID}/execute-operation`,
|
endpoint: `/api/oss/${itemID}/execute-operation`,
|
||||||
|
@ -104,8 +139,18 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
relocateConstituents: (data: ICstRelocateDTO) =>
|
moveItems: ({ itemID, data }: { itemID: number; data: IMoveItemsDTO }) =>
|
||||||
axiosPost<ICstRelocateDTO, IOperationSchemaDTO>({
|
axiosPatch<IMoveItemsDTO, IOperationSchemaDTO>({
|
||||||
|
schema: schemaOperationSchema,
|
||||||
|
endpoint: `/api/oss/${itemID}/move-items`,
|
||||||
|
request: {
|
||||||
|
data: data,
|
||||||
|
successMessage: infoMsg.moveSuccess
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
relocateConstituents: (data: IRelocateConstituentsDTO) =>
|
||||||
|
axiosPost<IRelocateConstituentsDTO, IOperationSchemaDTO>({
|
||||||
schema: schemaOperationSchema,
|
schema: schemaOperationSchema,
|
||||||
endpoint: `/api/oss/relocate-constituents`,
|
endpoint: `/api/oss/relocate-constituents`,
|
||||||
request: {
|
request: {
|
||||||
|
@ -119,4 +164,4 @@ export const ossApi = {
|
||||||
endpoint: '/api/oss/get-predecessor',
|
endpoint: '/api/oss/get-predecessor',
|
||||||
request: { data: { target: cstID } }
|
request: { data: { target: cstID } }
|
||||||
})
|
})
|
||||||
};
|
} as const;
|
||||||
|
|
|
@ -5,24 +5,25 @@
|
||||||
import { type ILibraryItem } from '@/features/library';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
|
|
||||||
import { Graph } from '@/models/graph';
|
import { Graph } from '@/models/graph';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
import { type IBlock, type IOperation, type IOperationSchema, type IOperationSchemaStats } from '../models/oss';
|
||||||
|
import { BLOCK_NODE_MIN_HEIGHT, BLOCK_NODE_MIN_WIDTH } from '../pages/oss-page/editor-oss-graph/graph/block-node';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO, OperationType } from './types';
|
import { type IOperationSchemaDTO, OperationType } from './types';
|
||||||
|
|
||||||
/**
|
/** Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}. */
|
||||||
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaDTO}.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class OssLoader {
|
export class OssLoader {
|
||||||
private oss: IOperationSchema;
|
private oss: IOperationSchema;
|
||||||
private graph: Graph = new Graph();
|
private graph: Graph = new Graph();
|
||||||
|
private hierarchy: Graph = new Graph();
|
||||||
private operationByID = new Map<number, IOperation>();
|
private operationByID = new Map<number, IOperation>();
|
||||||
|
private blockByID = new Map<number, IBlock>();
|
||||||
private schemaIDs: number[] = [];
|
private schemaIDs: number[] = [];
|
||||||
private items: ILibraryItem[];
|
private items: RO<ILibraryItem[]>;
|
||||||
|
|
||||||
constructor(input: IOperationSchemaDTO, items: ILibraryItem[]) {
|
constructor(input: RO<IOperationSchemaDTO>, items: RO<ILibraryItem[]>) {
|
||||||
this.oss = input as unknown as IOperationSchema;
|
this.oss = structuredClone(input) as IOperationSchema;
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,9 +33,12 @@ export class OssLoader {
|
||||||
this.createGraph();
|
this.createGraph();
|
||||||
this.extractSchemas();
|
this.extractSchemas();
|
||||||
this.inferOperationAttributes();
|
this.inferOperationAttributes();
|
||||||
|
this.inferBlockAttributes();
|
||||||
|
|
||||||
result.operationByID = this.operationByID;
|
result.operationByID = this.operationByID;
|
||||||
|
result.blockByID = this.blockByID;
|
||||||
result.graph = this.graph;
|
result.graph = this.graph;
|
||||||
|
result.hierarchy = this.hierarchy;
|
||||||
result.schemas = this.schemaIDs;
|
result.schemas = this.schemaIDs;
|
||||||
result.stats = this.calculateStats();
|
result.stats = this.calculateStats();
|
||||||
return result;
|
return result;
|
||||||
|
@ -44,6 +48,17 @@ export class OssLoader {
|
||||||
this.oss.operations.forEach(operation => {
|
this.oss.operations.forEach(operation => {
|
||||||
this.operationByID.set(operation.id, operation);
|
this.operationByID.set(operation.id, operation);
|
||||||
this.graph.addNode(operation.id);
|
this.graph.addNode(operation.id);
|
||||||
|
this.hierarchy.addNode(operation.id);
|
||||||
|
if (operation.parent) {
|
||||||
|
this.hierarchy.addEdge(-operation.parent, operation.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.oss.blocks.forEach(block => {
|
||||||
|
this.blockByID.set(block.id, block);
|
||||||
|
this.hierarchy.addNode(-block.id);
|
||||||
|
if (block.parent) {
|
||||||
|
this.hierarchy.addEdge(-block.parent, -block.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +86,16 @@ export class OssLoader {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inferBlockAttributes() {
|
||||||
|
this.oss.blocks.forEach(block => {
|
||||||
|
const geometry = this.oss.layout.blocks.find(item => item.id === block.id);
|
||||||
|
block.x = geometry?.x ?? 0;
|
||||||
|
block.y = geometry?.y ?? 0;
|
||||||
|
block.width = geometry?.width ?? BLOCK_NODE_MIN_WIDTH;
|
||||||
|
block.height = geometry?.height ?? BLOCK_NODE_MIN_HEIGHT;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private inferConsolidation(operationID: number): boolean {
|
private inferConsolidation(operationID: number): boolean {
|
||||||
const inputs = this.graph.expandInputs([operationID]);
|
const inputs = this.graph.expandInputs([operationID]);
|
||||||
if (inputs.length === 0) {
|
if (inputs.length === 0) {
|
||||||
|
@ -85,13 +110,14 @@ export class OssLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateStats(): IOperationSchemaStats {
|
private calculateStats(): IOperationSchemaStats {
|
||||||
const items = this.oss.operations;
|
const operations = this.oss.operations;
|
||||||
return {
|
return {
|
||||||
count_operations: items.length,
|
count_all: this.oss.operations.length + this.oss.blocks.length,
|
||||||
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
|
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
|
||||||
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
|
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
|
||||||
count_schemas: this.schemaIDs.length,
|
count_schemas: this.schemaIDs.length,
|
||||||
count_owned: items.filter(item => !!item.result && item.is_owned).length
|
count_owned: operations.filter(item => !!item.result && item.is_owned).length,
|
||||||
|
count_block: this.oss.blocks.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { schemaLibraryItem } from '@/features/library/backend/types';
|
import { schemaLibraryItem } from '@/features/library/backend/types';
|
||||||
import { schemaCstSubstitute } from '@/features/rsform/backend/types';
|
import { schemaSubstituteConstituents } from '@/features/rsform/backend/types';
|
||||||
|
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export type ICstSubstituteInfo = z.infer<typeof schemaCstSubstituteInfo>;
|
||||||
/** Represents {@link IOperation} data from server. */
|
/** Represents {@link IOperation} data from server. */
|
||||||
export type IOperationDTO = z.infer<typeof schemaOperation>;
|
export type IOperationDTO = z.infer<typeof schemaOperation>;
|
||||||
|
|
||||||
/** Represents {@link IOperation} data from server. */
|
/** Represents {@link IBlock} data from server. */
|
||||||
export type IBlockDTO = z.infer<typeof schemaBlock>;
|
export type IBlockDTO = z.infer<typeof schemaBlock>;
|
||||||
|
|
||||||
/** Represents backend data for {@link IOperationSchema}. */
|
/** Represents backend data for {@link IOperationSchema}. */
|
||||||
|
@ -27,33 +27,47 @@ export type IOperationSchemaDTO = z.infer<typeof schemaOperationSchema>;
|
||||||
/** Represents {@link IOperationSchema} layout. */
|
/** Represents {@link IOperationSchema} layout. */
|
||||||
export type IOssLayout = z.infer<typeof schemaOssLayout>;
|
export type IOssLayout = z.infer<typeof schemaOssLayout>;
|
||||||
|
|
||||||
/** Represents {@link IOperation} data, used in creation process. */
|
/** Represents {@link IBlock} data, used in Create action. */
|
||||||
export type IOperationCreateDTO = z.infer<typeof schemaOperationCreate>;
|
export type ICreateBlockDTO = z.infer<typeof schemaCreateBlock>;
|
||||||
|
|
||||||
|
/** Represents data response when creating {@link IBlock}. */
|
||||||
|
export type IBlockCreatedResponse = z.infer<typeof schemaBlockCreatedResponse>;
|
||||||
|
|
||||||
|
/** Represents {@link IBlock} data, used in Update action. */
|
||||||
|
export type IUpdateBlockDTO = z.infer<typeof schemaUpdateBlock>;
|
||||||
|
|
||||||
|
/** Represents {@link IBlock} data, used in Delete action. */
|
||||||
|
export type IDeleteBlockDTO = z.infer<typeof schemaDeleteBlock>;
|
||||||
|
|
||||||
|
/** Represents data, used to move {@link IOssItem} to another parent. */
|
||||||
|
export type IMoveItemsDTO = z.infer<typeof schemaMoveItems>;
|
||||||
|
|
||||||
|
/** Represents {@link IOperation} data, used in Create action. */
|
||||||
|
export type ICreateOperationDTO = z.infer<typeof schemaCreateOperation>;
|
||||||
|
|
||||||
/** Represents data response when creating {@link IOperation}. */
|
/** Represents data response when creating {@link IOperation}. */
|
||||||
export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedResponse>;
|
export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedResponse>;
|
||||||
/**
|
|
||||||
* Represents target {@link IOperation}.
|
/** Represents {@link IOperation} data, used in Update action. */
|
||||||
*/
|
export type IUpdateOperationDTO = z.infer<typeof schemaUpdateOperation>;
|
||||||
|
|
||||||
|
/** Represents {@link IOperation} data, used in Delete action. */
|
||||||
|
export type IDeleteOperationDTO = z.infer<typeof schemaDeleteOperation>;
|
||||||
|
|
||||||
|
/** Represents target {@link IOperation}. */
|
||||||
export interface ITargetOperation {
|
export interface ITargetOperation {
|
||||||
layout: IOssLayout;
|
layout: IOssLayout;
|
||||||
target: number;
|
target: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Represents {@link IOperation} data, used in destruction process. */
|
|
||||||
export type IOperationDeleteDTO = z.infer<typeof schemaOperationDelete>;
|
|
||||||
|
|
||||||
/** Represents data response when creating {@link IRSForm} for Input {@link IOperation}. */
|
/** Represents data response when creating {@link IRSForm} for Input {@link IOperation}. */
|
||||||
export type IInputCreatedResponse = z.infer<typeof schemaInputCreatedResponse>;
|
export type IInputCreatedResponse = z.infer<typeof schemaInputCreatedResponse>;
|
||||||
|
|
||||||
/** Represents {@link IOperation} data, used in setInput process. */
|
/** Represents {@link IOperation} data, used in setInput action. */
|
||||||
export type IInputUpdateDTO = z.infer<typeof schemaInputUpdate>;
|
export type IUpdateInputDTO = z.infer<typeof schemaUpdateInput>;
|
||||||
|
|
||||||
/** Represents {@link IOperation} data, used in update process. */
|
|
||||||
export type IOperationUpdateDTO = z.infer<typeof schemaOperationUpdate>;
|
|
||||||
|
|
||||||
/** Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s. */
|
/** Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s. */
|
||||||
export type ICstRelocateDTO = z.infer<typeof schemaCstRelocate>;
|
export type IRelocateConstituentsDTO = z.infer<typeof schemaRelocateConstituents>;
|
||||||
|
|
||||||
/** Represents {@link IConstituenta} reference. */
|
/** Represents {@link IConstituenta} reference. */
|
||||||
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
export type IConstituentaReference = z.infer<typeof schemaConstituentaReference>;
|
||||||
|
@ -80,7 +94,7 @@ export const schemaBlock = z.strictObject({
|
||||||
parent: z.number().nullable()
|
parent: z.number().nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaCstSubstituteInfo = schemaCstSubstitute.extend({
|
export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
|
||||||
operation: z.number(),
|
operation: z.number(),
|
||||||
original_alias: z.string(),
|
original_alias: z.string(),
|
||||||
original_term: z.string(),
|
original_term: z.string(),
|
||||||
|
@ -121,7 +135,42 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
|
||||||
substitutions: z.array(schemaCstSubstituteInfo)
|
substitutions: z.array(schemaCstSubstituteInfo)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaOperationCreate = z.strictObject({
|
export const schemaCreateBlock = z.strictObject({
|
||||||
|
layout: schemaOssLayout,
|
||||||
|
item_data: z.strictObject({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
parent: z.number().nullable()
|
||||||
|
}),
|
||||||
|
position_x: z.number(),
|
||||||
|
position_y: z.number(),
|
||||||
|
width: z.number(),
|
||||||
|
height: z.number(),
|
||||||
|
children_operations: z.array(z.number()),
|
||||||
|
children_blocks: z.array(z.number())
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaBlockCreatedResponse = z.strictObject({
|
||||||
|
new_block: schemaBlock,
|
||||||
|
oss: schemaOperationSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaUpdateBlock = z.strictObject({
|
||||||
|
target: z.number(),
|
||||||
|
layout: schemaOssLayout,
|
||||||
|
item_data: z.strictObject({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
parent: z.number().nullable()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaDeleteBlock = z.strictObject({
|
||||||
|
target: z.number(),
|
||||||
|
layout: schemaOssLayout
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaCreateOperation = z.strictObject({
|
||||||
layout: schemaOssLayout,
|
layout: schemaOssLayout,
|
||||||
item_data: z.strictObject({
|
item_data: z.strictObject({
|
||||||
alias: z.string().nonempty(),
|
alias: z.string().nonempty(),
|
||||||
|
@ -142,14 +191,34 @@ export const schemaOperationCreatedResponse = z.strictObject({
|
||||||
oss: schemaOperationSchema
|
oss: schemaOperationSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaOperationDelete = z.strictObject({
|
export const schemaUpdateOperation = z.strictObject({
|
||||||
|
target: z.number(),
|
||||||
|
layout: schemaOssLayout,
|
||||||
|
item_data: z.strictObject({
|
||||||
|
alias: z.string().nonempty(errorMsg.requiredField),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
parent: z.number().nullable()
|
||||||
|
}),
|
||||||
|
arguments: z.array(z.number()),
|
||||||
|
substitutions: z.array(schemaSubstituteConstituents)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaDeleteOperation = z.strictObject({
|
||||||
target: z.number(),
|
target: z.number(),
|
||||||
layout: schemaOssLayout,
|
layout: schemaOssLayout,
|
||||||
keep_constituents: z.boolean(),
|
keep_constituents: z.boolean(),
|
||||||
delete_schema: z.boolean()
|
delete_schema: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaInputUpdate = z.strictObject({
|
export const schemaMoveItems = z.strictObject({
|
||||||
|
layout: schemaOssLayout,
|
||||||
|
operations: z.array(z.number()),
|
||||||
|
blocks: z.array(z.number()),
|
||||||
|
destination: z.number().nullable()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schemaUpdateInput = z.strictObject({
|
||||||
target: z.number(),
|
target: z.number(),
|
||||||
layout: schemaOssLayout,
|
layout: schemaOssLayout,
|
||||||
input: z.number().nullable()
|
input: z.number().nullable()
|
||||||
|
@ -160,19 +229,7 @@ export const schemaInputCreatedResponse = z.strictObject({
|
||||||
oss: schemaOperationSchema
|
oss: schemaOperationSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const schemaOperationUpdate = z.strictObject({
|
export const schemaRelocateConstituents = z.strictObject({
|
||||||
target: z.number(),
|
|
||||||
layout: schemaOssLayout,
|
|
||||||
item_data: z.strictObject({
|
|
||||||
alias: z.string().nonempty(errorMsg.requiredField),
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string()
|
|
||||||
}),
|
|
||||||
arguments: z.array(z.number()),
|
|
||||||
substitutions: z.array(schemaCstSubstitute)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const schemaCstRelocate = z.strictObject({
|
|
||||||
destination: z.number().nullable(),
|
destination: z.number().nullable(),
|
||||||
items: z.array(z.number()).refine(data => data.length > 0)
|
items: z.array(z.number()).refine(data => data.length > 0)
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,14 +5,14 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type IOperationCreateDTO } from './types';
|
import { type ICreateBlockDTO } from './types';
|
||||||
|
|
||||||
export const useOperationCreate = () => {
|
export const useCreateBlock = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const { updateTimestamp } = useUpdateTimestamp();
|
const { updateTimestamp } = useUpdateTimestamp();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-create'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-block'],
|
||||||
mutationFn: ossApi.operationCreate,
|
mutationFn: ossApi.createBlock,
|
||||||
onSuccess: response => {
|
onSuccess: response => {
|
||||||
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
|
||||||
updateTimestamp(response.oss.id);
|
updateTimestamp(response.oss.id);
|
||||||
|
@ -20,6 +20,6 @@ export const useOperationCreate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
operationCreate: (data: { itemID: number; data: IOperationCreateDTO }) => mutation.mutateAsync(data)
|
createBlock: (data: { itemID: number; data: ICreateBlockDTO }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -5,11 +5,11 @@ import { KEYS } from '@/backend/configuration';
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type ITargetOperation } from './types';
|
import { type ITargetOperation } from './types';
|
||||||
|
|
||||||
export const useInputCreate = () => {
|
export const useCreateInput = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'input-create'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-input'],
|
||||||
mutationFn: ossApi.inputCreate,
|
mutationFn: ossApi.createInput,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
@ -20,7 +20,7 @@ export const useInputCreate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
inputCreate: (data: { itemID: number; data: ITargetOperation }) =>
|
createInput: (data: { itemID: number; data: ITargetOperation }) =>
|
||||||
mutation.mutateAsync(data).then(response => response.new_schema)
|
mutation.mutateAsync(data).then(response => response.new_schema)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
import { ossApi } from './api';
|
||||||
|
import { type ICreateOperationDTO } from './types';
|
||||||
|
|
||||||
|
export const useCreateOperation = () => {
|
||||||
|
const client = useQueryClient();
|
||||||
|
const { updateTimestamp } = useUpdateTimestamp();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-operation'],
|
||||||
|
mutationFn: ossApi.createOperation,
|
||||||
|
onSuccess: response => {
|
||||||
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
|
||||||
|
updateTimestamp(response.oss.id);
|
||||||
|
},
|
||||||
|
onError: () => client.invalidateQueries()
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
createOperation: (data: { itemID: number; data: ICreateOperationDTO }) => mutation.mutateAsync(data)
|
||||||
|
};
|
||||||
|
};
|
|
@ -3,13 +3,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type IInputUpdateDTO } from './types';
|
import { type IDeleteBlockDTO } from './types';
|
||||||
|
|
||||||
export const useInputUpdate = () => {
|
export const useDeleteBlock = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'input-update'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-block'],
|
||||||
mutationFn: ossApi.inputUpdate,
|
mutationFn: ossApi.deleteBlock,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
@ -20,6 +20,6 @@ export const useInputUpdate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
inputUpdate: (data: { itemID: number; data: IInputUpdateDTO }) => mutation.mutateAsync(data)
|
deleteBlock: (data: { itemID: number; data: IDeleteBlockDTO }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -3,13 +3,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type IOperationDeleteDTO } from './types';
|
import { type IDeleteOperationDTO } from './types';
|
||||||
|
|
||||||
export const useOperationDelete = () => {
|
export const useDeleteOperation = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-delete'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-operation'],
|
||||||
mutationFn: ossApi.operationDelete,
|
mutationFn: ossApi.deleteOperation,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
@ -20,6 +20,6 @@ export const useOperationDelete = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
operationDelete: (data: { itemID: number; data: IOperationDeleteDTO }) => mutation.mutateAsync(data)
|
deleteOperation: (data: { itemID: number; data: IDeleteOperationDTO }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -5,11 +5,11 @@ import { KEYS } from '@/backend/configuration';
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type ITargetOperation } from './types';
|
import { type ITargetOperation } from './types';
|
||||||
|
|
||||||
export const useOperationExecute = () => {
|
export const useExecuteOperation = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-execute'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'execute-operation'],
|
||||||
mutationFn: ossApi.operationExecute,
|
mutationFn: ossApi.executeOperation,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
||||||
return Promise.allSettled([
|
return Promise.allSettled([
|
||||||
|
@ -20,6 +20,6 @@ export const useOperationExecute = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
operationExecute: (data: { itemID: number; data: ITargetOperation }) => mutation.mutateAsync(data)
|
executeOperation: (data: { itemID: number; data: ITargetOperation }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
import { ossApi } from './api';
|
||||||
|
import { type IMoveItemsDTO } from './types';
|
||||||
|
|
||||||
|
export const useMoveItems = () => {
|
||||||
|
const client = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'move-items'],
|
||||||
|
mutationFn: ossApi.moveItems,
|
||||||
|
onSuccess: data => {
|
||||||
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
||||||
|
return Promise.allSettled([
|
||||||
|
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
|
||||||
|
client.invalidateQueries({ queryKey: [KEYS.rsform] })
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
onError: () => client.invalidateQueries()
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
moveItems: (data: { itemID: number; data: IMoveItemsDTO }) => mutation.mutateAsync(data)
|
||||||
|
};
|
||||||
|
};
|
|
@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type ICstRelocateDTO } from './types';
|
import { type IRelocateConstituentsDTO } from './types';
|
||||||
|
|
||||||
export const useRelocateConstituents = () => {
|
export const useRelocateConstituents = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
@ -20,6 +20,6 @@ export const useRelocateConstituents = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
relocateConstituents: (data: ICstRelocateDTO) => mutation.mutateAsync(data)
|
relocateConstituents: (data: IRelocateConstituentsDTO) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,13 +5,13 @@ import { type ILibraryItem } from '@/features/library';
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type IOperationUpdateDTO } from './types';
|
import { type IUpdateBlockDTO } from './types';
|
||||||
|
|
||||||
export const useOperationUpdate = () => {
|
export const useUpdateBlock = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'operation-update'],
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-block'],
|
||||||
mutationFn: ossApi.operationUpdate,
|
mutationFn: ossApi.updateBlock,
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
|
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
|
||||||
const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
|
const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
|
||||||
|
@ -32,6 +32,6 @@ export const useOperationUpdate = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
operationUpdate: (data: { itemID: number; data: IOperationUpdateDTO }) => mutation.mutateAsync(data)
|
updateBlock: (data: { itemID: number; data: IUpdateBlockDTO }) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
import { ossApi } from './api';
|
||||||
|
import { type IUpdateInputDTO } from './types';
|
||||||
|
|
||||||
|
export const useUpdateInput = () => {
|
||||||
|
const client = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-input'],
|
||||||
|
mutationFn: ossApi.updateInput,
|
||||||
|
onSuccess: data => {
|
||||||
|
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
|
||||||
|
return Promise.allSettled([
|
||||||
|
client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
|
||||||
|
client.invalidateQueries({ queryKey: [KEYS.rsform] })
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
onError: () => client.invalidateQueries()
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
updateInput: (data: { itemID: number; data: IUpdateInputDTO }) => mutation.mutateAsync(data)
|
||||||
|
};
|
||||||
|
};
|
|
@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
import { type IOperationSchemaDTO, type IOssLayout } from './types';
|
import { type IOperationSchemaDTO, type IOssLayout } from './types';
|
||||||
|
@ -17,7 +18,7 @@ export const useUpdateLayout = () => {
|
||||||
updateTimestamp(variables.itemID);
|
updateTimestamp(variables.itemID);
|
||||||
client.setQueryData(
|
client.setQueryData(
|
||||||
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
|
ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey,
|
||||||
(prev: IOperationSchemaDTO | undefined) =>
|
(prev: RO<IOperationSchemaDTO> | undefined) =>
|
||||||
!prev
|
!prev
|
||||||
? prev
|
? prev
|
||||||
: {
|
: {
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { type ILibraryItem } from '@/features/library';
|
||||||
|
|
||||||
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
import { ossApi } from './api';
|
||||||
|
import { type IUpdateOperationDTO } from './types';
|
||||||
|
|
||||||
|
export const useUpdateOperation = () => {
|
||||||
|
const client = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-operation'],
|
||||||
|
mutationFn: ossApi.updateOperation,
|
||||||
|
onSuccess: (data, variables) => {
|
||||||
|
client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data);
|
||||||
|
const schemaID = data.operations.find(item => item.id === variables.data.target)?.result;
|
||||||
|
if (!schemaID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.setQueryData(KEYS.composite.libraryList, (prev: ILibraryItem[] | undefined) =>
|
||||||
|
!prev
|
||||||
|
? undefined
|
||||||
|
: prev.map(item =>
|
||||||
|
item.id === schemaID ? { ...item, ...variables.data.item_data, time_update: Date() } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return client.invalidateQueries({
|
||||||
|
queryKey: KEYS.composite.rsItem({ itemID: schemaID })
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => client.invalidateQueries()
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
updateOperation: (data: { itemID: number; data: IUpdateOperationDTO }) => mutation.mutateAsync(data)
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type IBlock } from '../models/oss';
|
||||||
|
|
||||||
|
interface InfoOperationProps {
|
||||||
|
block: IBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoBlock({ block }: InfoOperationProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{block.title ? (
|
||||||
|
<p>
|
||||||
|
<b>Название: </b>
|
||||||
|
{block.title}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{block.description ? (
|
||||||
|
<p>
|
||||||
|
<b>Описание: </b>
|
||||||
|
{block.description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
import { Tooltip } from '@/components/container';
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { useOperationTooltipStore } from '../stores/operation-tooltip';
|
|
||||||
|
|
||||||
import { InfoOperation } from './info-operation';
|
|
||||||
|
|
||||||
export function OperationTooltip() {
|
|
||||||
const hoverOperation = useOperationTooltipStore(state => state.activeOperation);
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
clickable
|
|
||||||
id={globalIDs.operation_tooltip}
|
|
||||||
layer='z-topmost'
|
|
||||||
className='max-w-140 dense max-h-120! overflow-y-auto!'
|
|
||||||
hidden={!hoverOperation}
|
|
||||||
>
|
|
||||||
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
159
rsconcept/frontend/src/features/oss/components/pick-contents.tsx
Normal file
159
rsconcept/frontend/src/features/oss/components/pick-contents.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { createColumnHelper, DataTable } from '@/components/data-table';
|
||||||
|
import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/icons';
|
||||||
|
import { ComboBox } from '@/components/input/combo-box';
|
||||||
|
import { type Styling } from '@/components/props';
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
import { NoData } from '@/components/view';
|
||||||
|
import { type RO } from '@/utils/meta';
|
||||||
|
|
||||||
|
import { labelOssItem } from '../labels';
|
||||||
|
import { type IOperationSchema, type IOssItem } from '../models/oss';
|
||||||
|
import { getItemID, isOperation } from '../models/oss-api';
|
||||||
|
|
||||||
|
const SELECTION_CLEAR_TIMEOUT = 1000;
|
||||||
|
|
||||||
|
interface PickMultiOperationProps extends Styling {
|
||||||
|
value: number[];
|
||||||
|
onChange: (newValue: number[]) => void;
|
||||||
|
schema: IOperationSchema;
|
||||||
|
rows?: number;
|
||||||
|
exclude?: number[];
|
||||||
|
disallowBlocks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<RO<IOssItem>>();
|
||||||
|
|
||||||
|
export function PickContents({
|
||||||
|
rows,
|
||||||
|
schema,
|
||||||
|
exclude,
|
||||||
|
value,
|
||||||
|
disallowBlocks,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
...restProps
|
||||||
|
}: PickMultiOperationProps) {
|
||||||
|
const selectedItems = value
|
||||||
|
.map(itemID => (itemID > 0 ? schema.operationByID.get(itemID) : schema.blockByID.get(-itemID)))
|
||||||
|
.filter(item => item !== undefined);
|
||||||
|
const [lastSelected, setLastSelected] = useState<RO<IOssItem> | null>(null);
|
||||||
|
const items = [
|
||||||
|
...(disallowBlocks ? [] : schema.blocks.filter(item => !value.includes(-item.id) && !exclude?.includes(-item.id))),
|
||||||
|
...schema.operations.filter(item => !value.includes(item.id) && !exclude?.includes(item.id))
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleDelete(target: number) {
|
||||||
|
onChange(value.filter(item => item !== target));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(target: RO<IOssItem> | null) {
|
||||||
|
if (target) {
|
||||||
|
setLastSelected(target);
|
||||||
|
onChange([...value, getItemID(target)]);
|
||||||
|
setTimeout(() => setLastSelected(null), SELECTION_CLEAR_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveUp(target: number) {
|
||||||
|
const index = value.indexOf(target);
|
||||||
|
if (index > 0) {
|
||||||
|
const newSelected = [...value];
|
||||||
|
newSelected[index] = newSelected[index - 1];
|
||||||
|
newSelected[index - 1] = target;
|
||||||
|
onChange(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMoveDown(target: number) {
|
||||||
|
const index = value.indexOf(target);
|
||||||
|
if (index < value.length - 1) {
|
||||||
|
const newSelected = [...value];
|
||||||
|
newSelected[index] = newSelected[index + 1];
|
||||||
|
newSelected[index + 1] = target;
|
||||||
|
onChange(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor(item => isOperation(item), {
|
||||||
|
id: 'type',
|
||||||
|
header: 'Тип',
|
||||||
|
size: 150,
|
||||||
|
minSize: 150,
|
||||||
|
maxSize: 150,
|
||||||
|
cell: props => <div>{isOperation(props.row.original) ? 'Операция' : 'Блок'}</div>
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('title', {
|
||||||
|
id: 'title',
|
||||||
|
header: 'Название',
|
||||||
|
size: 1200,
|
||||||
|
minSize: 300,
|
||||||
|
maxSize: 1200,
|
||||||
|
cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
size: 0,
|
||||||
|
cell: props => (
|
||||||
|
<div className='flex w-fit'>
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||||
|
onClick={() => handleDelete(getItemID(props.row.original))}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Переместить выше'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||||
|
onClick={() => handleMoveUp(getItemID(props.row.original))}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title='Переместить ниже'
|
||||||
|
noHover
|
||||||
|
className='px-0'
|
||||||
|
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||||
|
onClick={() => handleMoveDown(getItemID(props.row.original))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col gap-1 border-t border-x rounded-md bg-input', className)} {...restProps}>
|
||||||
|
<ComboBox
|
||||||
|
noBorder
|
||||||
|
items={items}
|
||||||
|
value={lastSelected}
|
||||||
|
placeholder='Выберите операцию или блок'
|
||||||
|
idFunc={item => String(getItemID(item))}
|
||||||
|
labelValueFunc={item => labelOssItem(item)}
|
||||||
|
labelOptionFunc={item => labelOssItem(item)}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
|
<DataTable
|
||||||
|
dense
|
||||||
|
noFooter
|
||||||
|
rows={rows}
|
||||||
|
contentHeight='1.3rem'
|
||||||
|
className='cc-scroll-y text-sm select-none border-y rounded-b-md'
|
||||||
|
data={selectedItems}
|
||||||
|
columns={columns}
|
||||||
|
headPosition='0rem'
|
||||||
|
noDataComponent={
|
||||||
|
<NoData>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
</NoData>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { ComboBox } from '@/components/input/combo-box';
|
||||||
|
import { type Styling } from '@/components/props';
|
||||||
|
|
||||||
|
import { type IBlock } from '../models/oss';
|
||||||
|
|
||||||
|
interface SelectBlockProps extends Styling {
|
||||||
|
id?: string;
|
||||||
|
value: IBlock | null;
|
||||||
|
onChange: (newValue: IBlock | null) => void;
|
||||||
|
|
||||||
|
items?: IBlock[];
|
||||||
|
placeholder?: string;
|
||||||
|
noBorder?: boolean;
|
||||||
|
popoverClassname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectBlock({ items, placeholder = 'Выберите блок', ...restProps }: SelectBlockProps) {
|
||||||
|
return (
|
||||||
|
<ComboBox
|
||||||
|
items={items}
|
||||||
|
clearable
|
||||||
|
placeholder={placeholder}
|
||||||
|
idFunc={block => String(block.id)}
|
||||||
|
labelValueFunc={block => block.title}
|
||||||
|
labelOptionFunc={block => block.title}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { IconConceptBlock } from '@/components/icons';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { type IBlock } from '../models/oss';
|
||||||
|
|
||||||
|
import { SelectBlock } from './select-block';
|
||||||
|
|
||||||
|
interface SelectParentProps {
|
||||||
|
id?: string;
|
||||||
|
value: IBlock | null;
|
||||||
|
onChange: (newValue: IBlock | null) => void;
|
||||||
|
|
||||||
|
fullWidth?: boolean;
|
||||||
|
items?: IBlock[];
|
||||||
|
placeholder?: string;
|
||||||
|
noBorder?: boolean;
|
||||||
|
popoverClassname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectParent({ fullWidth, ...restProps }: SelectParentProps) {
|
||||||
|
return (
|
||||||
|
<div className={clsx('flex gap-2 items-center', !fullWidth ? 'w-80' : 'w-full')}>
|
||||||
|
<IconConceptBlock
|
||||||
|
tabIndex={-1}
|
||||||
|
size='2rem'
|
||||||
|
className='text-primary min-w-8'
|
||||||
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
|
data-tooltip-content='Родительский блок содержания'
|
||||||
|
/>
|
||||||
|
<SelectBlock className={fullWidth ? 'grow' : 'w-70'} {...restProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user