diff --git a/rsconcept/backend/apps/rsform/migrations/0001_initial.py b/rsconcept/backend/apps/rsform/migrations/0001_initial.py index 8678babf..a5f027c1 100644 --- a/rsconcept/backend/apps/rsform/migrations/0001_initial.py +++ b/rsconcept/backend/apps/rsform/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-07-23 11:55 +# Generated by Django 4.2.3 on 2023-07-24 19:06 import apps.rsform.models from django.conf import settings @@ -37,13 +37,16 @@ class Migration(migrations.Migration): name='Constituenta', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')), - ('alias', models.CharField(max_length=8, verbose_name='Имя')), + ('order', models.PositiveIntegerField(default=-1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')), + ('alias', models.CharField(default='undefined', max_length=8, verbose_name='Имя')), ('csttype', models.CharField(choices=[('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип')), ('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')), - ('term', models.JSONField(default=apps.rsform.models._empty_term, verbose_name='Термин')), + ('term_raw', models.TextField(blank=True, default='', verbose_name='Термин (с отсылками)')), + ('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')), + ('term_forms', models.JSONField(default=apps.rsform.models._empty_forms, verbose_name='Словоформы')), ('definition_formal', models.TextField(blank=True, default='', verbose_name='Родоструктурное определение')), - ('definition_text', models.JSONField(blank=True, default=apps.rsform.models._empty_definition, verbose_name='Текстовое определние')), + ('definition_raw', models.TextField(blank=True, default='', verbose_name='Текстовое определние (с отсылками)')), + ('definition_resolved', models.TextField(blank=True, default='', verbose_name='Текстовое определние')), ('schema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема')), ], options={ diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 106274e6..78b0bacc 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -2,6 +2,7 @@ import json from django.db import models, transaction from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError +from django.urls import reverse from apps.users.models import User import pyconcept @@ -26,12 +27,8 @@ class Syntax(models.TextChoices): MATH = 'math' -def _empty_term(): - return {'raw': '', 'resolved': '', 'forms': []} - - -def _empty_definition(): - return {'raw': '', 'resolved': ''} +def _empty_forms(): + return [] class RSForm(models.Model): @@ -91,7 +88,7 @@ class RSForm(models.Model): alias=alias, csttype=type ) - self._recreate_order() + self._update_from_core() self.save() return Constituenta.objects.get(pk=result.pk) @@ -107,16 +104,40 @@ class RSForm(models.Model): alias=alias, csttype=type ) - self._recreate_order() + self._update_from_core() self.save() return Constituenta.objects.get(pk=result.pk) + @transaction.atomic + def move_cst(self, listCst: list['Constituenta'], target: int): + ''' Move list of constituents to specific position ''' + count_moved = 0 + count_top = 0 + count_bot = 0 + size = len(listCst) + update_list = [] + for cst in self.constituents(): + if cst not in listCst: + if count_top + 1 < target: + cst.order = count_top + 1 + count_top += 1 + else: + cst.order = target + size + count_bot + count_bot += 1 + else: + cst.order = target + count_moved + count_moved += 1 + update_list.append(cst) + Constituenta.objects.bulk_update(update_list, ['order']) + self._update_from_core() + self.save() + @transaction.atomic def delete_cst(self, listCst): ''' Delete multiple constituents. Do not check if listCst are from this schema ''' for cst in listCst: cst.delete() - self._recreate_order() + self._update_from_core() self.save() @staticmethod @@ -143,6 +164,9 @@ class RSForm(models.Model): def __str__(self): return self.title + def get_absolute_url(self): + return reverse('rsform-detail', kwargs={'pk': self.pk}) + def _prepare_json_rsform(self: 'Constituenta') -> dict: return { 'type': 'rsform', @@ -152,7 +176,7 @@ class RSForm(models.Model): 'items': [] } - def _recreate_order(self): + def _update_from_core(self) -> dict: checked = json.loads(pyconcept.check_schema(json.dumps(self.to_json()))) update_list = self.constituents().only('id', 'order') if (len(checked['items']) != update_list.count()): @@ -166,22 +190,12 @@ class RSForm(models.Model): order += 1 break Constituenta.objects.bulk_update(update_list, ['order']) + return checked def _create_cst_from_json(self, items): order = 1 for cst in items: - # TODO: get rid of empty_term etc. Use None instead - Constituenta.objects.create( - alias=cst['alias'], - schema=self, - order=order, - csttype=cst['cstType'], - convention=cst.get('convention', 'Без названия'), - definition_formal=cst['definition'].get('formal', '') if 'definition' in cst else '', - term=cst.get('term', _empty_term()), - definition_text=cst['definition']['text'] \ - if 'definition' in cst and 'text' in cst['definition'] else _empty_definition() # noqa: E502 - ) + Constituenta.import_json(cst, self, order) order += 1 @@ -194,11 +208,13 @@ class Constituenta(models.Model): ) order = models.PositiveIntegerField( verbose_name='Позиция', - validators=[MinValueValidator(1)] + validators=[MinValueValidator(1)], + default=-1, ) alias = models.CharField( verbose_name='Имя', - max_length=8 + max_length=8, + default='undefined' ) csttype = models.CharField( verbose_name='Тип', @@ -211,18 +227,33 @@ class Constituenta(models.Model): default='', blank=True ) - term = models.JSONField( + term_raw = models.TextField( + verbose_name='Термин (с отсылками)', + default='', + blank=True + ) + term_resolved = models.TextField( verbose_name='Термин', - default=_empty_term + default='', + blank=True + ) + term_forms = models.JSONField( + verbose_name='Словоформы', + default=_empty_forms ) definition_formal = models.TextField( verbose_name='Родоструктурное определение', default='', blank=True ) - definition_text = models.JSONField( + definition_raw = models.TextField( + verbose_name='Текстовое определние (с отсылками)', + default='', + blank=True + ) + definition_resolved = models.TextField( verbose_name='Текстовое определние', - default=_empty_definition, + default='', blank=True ) @@ -230,9 +261,34 @@ class Constituenta(models.Model): verbose_name = 'Конституета' verbose_name_plural = 'Конституенты' + def get_absolute_url(self): + return reverse('constituenta-detail', kwargs={'pk': self.pk}) + def __str__(self): return self.alias + @staticmethod + def import_json(data: dict, schema: RSForm, order: int) -> 'Constituenta': + cst = Constituenta( + alias=data['alias'], + schema=schema, + order=order, + csttype=data['cstType'], + convention=data.get('convention', 'Без названия') + ) + if 'definition' in data: + if 'formal' in data['definition']: + cst.definition_formal = data['definition']['formal'] + if 'text' in data['definition']: + cst.definition_raw = data['definition']['text'].get('raw', '') + cst.definition_resolved = data['definition']['text'].get('resolved', '') + if 'term' in data: + cst.term_raw = data['definition']['text'].get('raw', '') + cst.term_resolved = data['definition']['text'].get('resolved', '') + cst.term_forms = data['definition']['text'].get('forms', []) + cst.save() + return cst + def to_json(self) -> str: return { 'entityUID': self.id, @@ -240,9 +296,16 @@ class Constituenta(models.Model): 'cstType': self.csttype, 'alias': self.alias, 'convention': self.convention, - 'term': self.term, + 'term': { + 'raw': self.term_raw, + 'resolved': self.term_resolved, + 'forms': self.term_forms, + }, 'definition': { 'formal': self.definition_formal, - 'text': self.definition_text - } + 'text': { + 'raw': self.definition_raw, + 'resolved': self.definition_resolved, + }, + }, } diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 281a199c..536e83c1 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -1,5 +1,7 @@ +import json from rest_framework import serializers +import pyconcept from .models import Constituenta, RSForm @@ -7,12 +9,6 @@ class FileSerializer(serializers.Serializer): file = serializers.FileField(allow_empty_file=False) -class ItemsListSerlializer(serializers.Serializer): - items = serializers.ListField( - child=serializers.IntegerField() - ) - - class ExpressionSerializer(serializers.Serializer): expression = serializers.CharField() @@ -35,7 +31,60 @@ class ConstituentaSerializer(serializers.ModelSerializer): return super().update(instance, validated_data) -class NewConstituentaSerializer(serializers.Serializer): +class StandaloneCstSerializer(serializers.ModelSerializer): + id = serializers.IntegerField() + + class Meta: + model = Constituenta + exclude = ('schema', ) + + def validate(self, attrs): + try: + attrs['object'] = Constituenta.objects.get(pk=attrs['id']) + except Constituenta.DoesNotExist: + raise serializers.ValidationError({f"{attrs['id']}": 'Конституента не существует'}) + return attrs + + +class CstCreateSerializer(serializers.Serializer): alias = serializers.CharField(max_length=8) csttype = serializers.CharField(max_length=10) insert_after = serializers.IntegerField(required=False) + + +class CstListSerlializer(serializers.Serializer): + items = serializers.ListField( + child=StandaloneCstSerializer() + ) + + def validate(self, attrs): + schema = self.context['schema'] + cstList = [] + for item in attrs['items']: + cst = item['object'] + if (cst.schema != schema): + raise serializers.ValidationError( + {'items': f'Конституенты должны относиться к данной схеме: {item}'}) + cstList.append(cst) + attrs['constituents'] = cstList + return attrs + + +class CstMoveSerlializer(CstListSerlializer): + move_to = serializers.IntegerField() + + +class RSFormDetailsSerlializer(serializers.BaseSerializer): + class Meta: + model = RSForm + + def to_representation(self, instance: RSForm): + trs = pyconcept.check_schema(json.dumps(instance.to_json())) + trs = trs.replace('entityUID', 'id') + result = json.loads(trs) + result['id'] = instance.id + result['time_update'] = instance.time_update + result['time_create'] = instance.time_create + result['is_common'] = instance.is_common + result['owner'] = (instance.owner.pk if instance.owner is not None else None) + return result diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index 5c158882..5c1091ed 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -8,8 +8,7 @@ from apps.rsform.models import ( RSForm, Constituenta, CstType, - User, - _empty_term, _empty_definition + User ) @@ -23,6 +22,11 @@ class TestConstituenta(TestCase): cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') self.assertEqual(str(cst), testStr) + def test_url(self): + testStr = 'X1' + cst = Constituenta.objects.create(alias=testStr, schema=self.schema1, order=1, convention='Test') + self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.id}/') + def test_order_not_null(self): with self.assertRaises(IntegrityError): Constituenta.objects.create(alias='X1', schema=self.schema1) @@ -52,28 +56,11 @@ class TestConstituenta(TestCase): self.assertEqual(cst.csttype, CstType.BASE) self.assertEqual(cst.convention, '') self.assertEqual(cst.definition_formal, '') - self.assertEqual(cst.term, _empty_term()) - self.assertEqual(cst.definition_text, _empty_definition()) - - def test_create(self): - cst = Constituenta.objects.create( - alias='S1', - schema=self.schema1, - order=1, - csttype=CstType.STRUCTURED, - convention='Test convention', - definition_formal='X1=X1', - term={'raw': 'Текст @{12|3}', 'resolved': 'Текст тест', 'forms': []}, - definition_text={'raw': 'Текст1 @{12|3}', 'resolved': 'Текст1 тест'}, - ) - self.assertEqual(cst.schema, self.schema1) - self.assertEqual(cst.order, 1) - self.assertEqual(cst.alias, 'S1') - self.assertEqual(cst.csttype, CstType.STRUCTURED) - self.assertEqual(cst.convention, 'Test convention') - self.assertEqual(cst.definition_formal, 'X1=X1') - self.assertEqual(cst.term, {'raw': 'Текст @{12|3}', 'resolved': 'Текст тест', 'forms': []}) - self.assertEqual(cst.definition_text, {'raw': 'Текст1 @{12|3}', 'resolved': 'Текст1 тест'}) + self.assertEqual(cst.term_raw, '') + self.assertEqual(cst.term_resolved, '') + self.assertEqual(cst.term_forms, []) + self.assertEqual(cst.definition_resolved, '') + self.assertEqual(cst.definition_raw, '') class TestRSForm(TestCase): @@ -87,6 +74,11 @@ class TestRSForm(TestCase): schema = RSForm.objects.create(title=testStr, owner=self.user1, alias='КС1') self.assertEqual(str(schema), testStr) + def test_url(self): + testStr = 'Test123' + schema = RSForm.objects.create(title=testStr, owner=self.user1, alias='КС1') + self.assertEqual(schema.get_absolute_url(), f'/api/rsforms/{schema.id}/') + def test_create_default(self): schema = RSForm.objects.create(title='Test') self.assertIsNone(schema.owner) @@ -191,6 +183,32 @@ class TestRSForm(TestCase): self.assertEqual(x1.order, 1) self.assertEqual(d2.order, 2) + def test_move_cst(self): + schema = RSForm.objects.create(title='Test') + x1 = schema.insert_last('X1', CstType.BASE) + x2 = schema.insert_last('X2', CstType.BASE) + d1 = schema.insert_last('D1', CstType.TERM) + d2 = schema.insert_last('D2', CstType.TERM) + schema.move_cst([x2, d2], 1) + x1.refresh_from_db() + x2.refresh_from_db() + d1.refresh_from_db() + d2.refresh_from_db() + self.assertEqual(x1.order, 2) + self.assertEqual(x2.order, 1) + self.assertEqual(d1.order, 4) + self.assertEqual(d2.order, 3) + + def test_move_cst_down(self): + schema = RSForm.objects.create(title='Test') + x1 = schema.insert_last('X1', CstType.BASE) + x2 = schema.insert_last('X2', CstType.BASE) + schema.move_cst([x1], 2) + x1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(x1.order, 2) + self.assertEqual(x2.order, 1) + def test_to_json(self): schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test') x1 = schema.insert_at(4, 'X1', CstType.BASE) diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 2376ae5e..2415603a 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -119,12 +119,14 @@ class TestRSFormViewset(APITestCase): def test_details(self): schema = RSForm.objects.create(title='Test') - schema.insert_at(1, 'X1', CstType.BASE) + cst = schema.insert_at(1, 'X1', CstType.BASE) + schema.insert_at(2, 'X2', CstType.BASE) response = self.client.get(f'/api/rsforms/{schema.id}/details/') self.assertEqual(response.status_code, 200) self.assertEqual(response.data['title'], 'Test') - self.assertEqual(len(response.data['items']), 1) + self.assertEqual(len(response.data['items']), 2) self.assertEqual(response.data['items'][0]['parse']['status'], 'verified') + self.assertEqual(response.data['items'][0]['id'], cst.id) def test_check(self): schema = RSForm.objects.create(title='Test') @@ -183,28 +185,28 @@ class TestRSFormViewset(APITestCase): response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/', data=data, content_type='application/json') self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['alias'], 'X3') - x3 = Constituenta.objects.get(alias=response.data['alias']) + self.assertEqual(response.data['new_cst']['alias'], 'X3') + x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x3.order, 3) data = json.dumps({'alias': 'X4', 'csttype': 'basic', 'insert_after': x2.id}) response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/', data=data, content_type='application/json') self.assertEqual(response.status_code, 201) - self.assertEqual(response.data['alias'], 'X4') - x4 = Constituenta.objects.get(alias=response.data['alias']) + self.assertEqual(response.data['new_cst']['alias'], 'X4') + x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x4.order, 3) def test_delete_constituenta(self): schema = self.rsform_owned - data = json.dumps({'items': [1337]}) + data = json.dumps({'items': [{'id': 1337}]}) response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/', data=data, content_type='application/json') - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 400) x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1) x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2) - data = json.dumps({'items': [x1.id]}) + data = json.dumps({'items': [{'id': x1.id}]}) response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/', data=data, content_type='application/json') x2.refresh_from_db() @@ -215,11 +217,36 @@ class TestRSFormViewset(APITestCase): self.assertEqual(x2.order, 1) x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1) - data = json.dumps({'items': [x3.id]}) + data = json.dumps({'items': [{'id': x3.id}]}) response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/', data=data, content_type='application/json') self.assertEqual(response.status_code, 400) + def test_move_constituenta(self): + schema = self.rsform_owned + data = json.dumps({'items': [{'id': 1337}], 'move_to': 1}) + response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 400) + + x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1) + x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2) + data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1}) + response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', + data=data, content_type='application/json') + x1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['id'], schema.id) + self.assertEqual(x1.order, 2) + self.assertEqual(x2.order, 1) + + x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1) + data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1}) + response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 400) + class TestFunctionalViews(APITestCase): def setUp(self): diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py index adffd2b6..56456d50 100644 --- a/rsconcept/backend/apps/rsform/urls.py +++ b/rsconcept/backend/apps/rsform/urls.py @@ -7,7 +7,7 @@ rsform_router = routers.SimpleRouter() rsform_router.register(r'rsforms', views.RSFormViewSet) urlpatterns = [ - path('constituents//', views.ConstituentAPIView.as_view()), + path('constituents//', views.ConstituentAPIView.as_view(), name='constituenta-detail'), path('rsforms/import-trs/', views.TrsImportView.as_view()), path('rsforms/create-detailed/', views.create_rsform), path('func/parse-expression/', views.parse_expression), diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index efb18aa4..618e34a9 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -54,7 +54,7 @@ class RSFormViewSet(viewsets.ModelViewSet): def cst_create(self, request, pk): ''' Create new constituenta ''' schema: models.RSForm = self.get_object() - serializer = serializers.NewConstituentaSerializer(data=request.data) + serializer = serializers.CstCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) if ('insert_after' in serializer.validated_data): cstafter = models.Constituenta.objects.get(pk=serializer.validated_data['insert_after']) @@ -63,28 +63,32 @@ class RSFormViewSet(viewsets.ModelViewSet): serializer.validated_data['csttype']) else: constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['csttype']) - return Response(status=201, data=constituenta.to_json()) + schema.refresh_from_db() + outSerializer = serializers.RSFormDetailsSerlializer(schema) + response = Response(status=201, data={'new_cst': constituenta.to_json(), 'schema': outSerializer.data}) + response['Location'] = constituenta.get_absolute_url() + return response @action(detail=True, methods=['post'], url_path='cst-multidelete') def cst_multidelete(self, request, pk): ''' Delete multiple constituents ''' schema: models.RSForm = self.get_object() - serializer = serializers.ItemsListSerlializer(data=request.data) + serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) - listCst = [] - # TODO: consider moving validation to serializer - try: - for id in serializer.validated_data['items']: - cst = models.Constituenta.objects.get(pk=id) - if (cst.schema != schema): - return Response({'error', 'Конституенты должны относиться к данной схеме'}, status=400) - listCst.append(cst) - except models.Constituenta.DoesNotExist: - return Response(status=404) - - schema.delete_cst(listCst) + schema.delete_cst(serializer.validated_data['constituents']) return Response(status=202) + @action(detail=True, methods=['patch'], url_path='cst-moveto') + def cst_moveto(self, request, pk): + ''' Delete multiple constituents ''' + schema: models.RSForm = self.get_object() + serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema}) + serializer.is_valid(raise_exception=True) + schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to']) + schema.refresh_from_db() + outSerializer = serializers.RSFormDetailsSerlializer(schema) + return Response(status=200, data=outSerializer.data) + @action(detail=True, methods=['post']) def claim(self, request, pk=None): schema: models.RSForm = self.get_object() @@ -93,7 +97,7 @@ class RSFormViewSet(viewsets.ModelViewSet): else: schema.owner = self.request.user schema.save() - return Response(status=200) + return Response(status=200, data=serializers.RSFormSerializer(schema).data) @action(detail=True, methods=['get']) def contents(self, request, pk): @@ -105,14 +109,8 @@ class RSFormViewSet(viewsets.ModelViewSet): def details(self, request, pk): ''' Detailed schema view including statuses ''' schema: models.RSForm = self.get_object() - result = pyconcept.check_schema(json.dumps(schema.to_json())) - output_data = json.loads(result) - output_data['id'] = schema.id - output_data['time_update'] = schema.time_update - output_data['time_create'] = schema.time_create - output_data['is_common'] = schema.is_common - output_data['owner'] = (schema.owner.pk if schema.owner is not None else None) - return Response(output_data) + serializer = serializers.RSFormDetailsSerlializer(schema) + return Response(serializer.data) @action(detail=True, methods=['post']) def check(self, request, pk): diff --git a/rsconcept/frontend/src/App.tsx b/rsconcept/frontend/src/App.tsx index 4f135839..a4b1f802 100644 --- a/rsconcept/frontend/src/App.tsx +++ b/rsconcept/frontend/src/App.tsx @@ -23,7 +23,6 @@ function App() { autoClose={3000} draggable={false} pauseOnFocusLoss={false} - limit={5} />
diff --git a/rsconcept/frontend/src/components/Common/ConceptTab.tsx b/rsconcept/frontend/src/components/Common/ConceptTab.tsx index b48e5893..d6ac9cb2 100644 --- a/rsconcept/frontend/src/components/Common/ConceptTab.tsx +++ b/rsconcept/frontend/src/components/Common/ConceptTab.tsx @@ -4,10 +4,7 @@ import type { TabProps } from 'react-tabs'; function ConceptTab({children, className, ...otherProps} : TabProps) { return ( {children} diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 08eeaee6..2f2b03a1 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -3,28 +3,34 @@ import { IConstituenta, IRSForm } from '../utils/models'; import { useRSFormDetails } from '../hooks/useRSFormDetails'; import { ErrorInfo } from '../components/BackendError'; import { useAuth } from './AuthContext'; -import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm, postDeleteConstituenta, postNewConstituenta } from '../utils/backendAPI'; +import { + BackendCallback, deleteRSForm, getTRSFile, + patchConstituenta, patchMoveConstituenta, patchRSForm, + postClaimRSForm, postDeleteConstituenta, postNewConstituenta +} from '../utils/backendAPI'; import { toast } from 'react-toastify'; interface IRSFormContext { schema?: IRSForm - active?: IConstituenta + activeCst?: IConstituenta + activeID?: number + error: ErrorInfo loading: boolean processing: boolean + isOwned: boolean isEditable: boolean isClaimable: boolean - forceAdmin: boolean - readonly: boolean + isReadonly: boolean isTracking: boolean + isForceAdmin: boolean - setActive: React.Dispatch> + setActiveID: React.Dispatch> toggleForceAdmin: () => void toggleReadonly: () => void toggleTracking: () => void - reload: () => Promise update: (data: any, callback?: BackendCallback) => Promise destroy: (callback?: BackendCallback) => Promise claim: (callback?: BackendCallback) => Promise @@ -33,36 +39,19 @@ interface IRSFormContext { cstUpdate: (data: any, callback?: BackendCallback) => Promise cstCreate: (data: any, callback?: BackendCallback) => Promise cstDelete: (data: any, callback?: BackendCallback) => Promise + cstMoveTo: (data: any, callback?: BackendCallback) => Promise } -export const RSFormContext = createContext({ - schema: undefined, - active: undefined, - error: undefined, - loading: false, - processing: false, - isOwned: false, - isEditable: false, - isClaimable: false, - forceAdmin: false, - readonly: false, - isTracking: true, - - setActive: () => {}, - toggleForceAdmin: () => {}, - toggleReadonly: () => {}, - toggleTracking: () => {}, - - reload: async () => {}, - update: async () => {}, - destroy: async () => {}, - claim: async () => {}, - download: async () => {}, - - cstUpdate: async () => {}, - cstCreate: async () => {}, - cstDelete: async () => {}, -}) +const RSFormContext = createContext(null); +export const useRSForm = () => { + const context = useContext(RSFormContext); + if (!context) { + throw new Error( + 'useRSForm has to be used within ' + ); + } + return context; +} interface RSFormStateProps { schemaID: string @@ -71,22 +60,27 @@ interface RSFormStateProps { export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { const { user } = useAuth(); - const { schema, reload, error, setError, loading } = useRSFormDetails({target: schemaID}); + const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({target: schemaID}); const [processing, setProcessing] = useState(false) - const [active, setActive] = useState(undefined); + const [activeID, setActiveID] = useState(undefined); - const [forceAdmin, setForceAdmin] = useState(false); - const [readonly, setReadonly] = useState(false); + const [isForceAdmin, setIsForceAdmin] = useState(false); + const [isReadonly, setIsReadonly] = useState(false); - const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema]); - const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]); + const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner]); + const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner]); const isEditable = useMemo( () => { return ( - !loading && !readonly && - (isOwned || (forceAdmin && user?.is_staff) || false) + !loading && !isReadonly && + (isOwned || (isForceAdmin && user?.is_staff) || false) ) - }, [user, readonly, forceAdmin, isOwned, loading]); + }, [user, isReadonly, isForceAdmin, isOwned, loading]); + + const activeCst = useMemo( + () => { + return schema?.items && schema?.items.find((cst) => cst.id === activeID); + }, [schema?.items, activeID]); const isTracking = useMemo( () => { @@ -106,9 +100,12 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSucccess: callback + onSucccess: async (response) => { + await reload(); + if (callback) callback(response); + } }); - }, [schemaID, setError]); + }, [schemaID, setError, reload]); const destroy = useCallback( async (callback?: BackendCallback) => { @@ -128,9 +125,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSucccess: callback + onSucccess: async (response) => { + schema!.owner = user!.id + schema!.time_update = response.data['time_update'] + setSchema(schema) + if (callback) callback(response); + } }); - }, [schemaID, setError]); + }, [schemaID, setError, schema, user, setSchema]); const download = useCallback( async (callback: BackendCallback) => { @@ -146,14 +148,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { const cstUpdate = useCallback( async (data: any, callback?: BackendCallback) => { setError(undefined); - patchConstituenta(String(active!.entityUID), { + patchConstituenta(String(activeID), { data: data, showError: true, setLoading: setProcessing, onError: error => setError(error), onSucccess: callback }); - }, [active, setError]); + }, [activeID, setError]); const cstCreate = useCallback( async (data: any, callback?: BackendCallback) => { @@ -163,9 +165,12 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSucccess: callback + onSucccess: async (response) => { + setSchema(response.data['schema']); + if (callback) callback(response); + } }); - }, [schemaID, setError]); + }, [schemaID, setError, setSchema]); const cstDelete = useCallback( async (data: any, callback?: BackendCallback) => { @@ -175,25 +180,42 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { showError: true, setLoading: setProcessing, onError: error => setError(error), - onSucccess: callback + onSucccess: async (response) => { + await reload(); + if (callback) callback(response); + } }); - }, [schemaID, setError]); + }, [schemaID, setError, reload]); + + const cstMoveTo = useCallback( + async (data: any, callback?: BackendCallback) => { + setError(undefined); + patchMoveConstituenta(schemaID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: error => setError(error), + onSucccess: (response) => { + setSchema(response.data); + if (callback) callback(response); + } + }); + }, [schemaID, setError, setSchema]); return ( setForceAdmin(prev => !prev), - toggleReadonly: () => setReadonly(prev => !prev), + activeID, activeCst, + setActiveID, + isForceAdmin, isReadonly, + toggleForceAdmin: () => setIsForceAdmin(prev => !prev), + toggleReadonly: () => setIsReadonly(prev => !prev), isOwned, isEditable, isClaimable, isTracking, toggleTracking, - reload, update, download, destroy, claim, - cstUpdate, cstCreate, cstDelete, + update, download, destroy, claim, + cstUpdate, cstCreate, cstDelete, cstMoveTo, }}> { children } ); -} - -export const useRSForm = () => useContext(RSFormContext); \ No newline at end of file +} \ No newline at end of file diff --git a/rsconcept/frontend/src/hooks/useRSFormDetails.ts b/rsconcept/frontend/src/hooks/useRSFormDetails.ts index ad699cbf..d42497af 100644 --- a/rsconcept/frontend/src/hooks/useRSFormDetails.ts +++ b/rsconcept/frontend/src/hooks/useRSFormDetails.ts @@ -4,14 +4,20 @@ import { ErrorInfo } from '../components/BackendError'; import { getRSFormDetails } from '../utils/backendAPI'; export function useRSFormDetails({target}: {target?: string}) { - const [schema, setSchema] = useState(); + const [schema, setInnerSchema] = useState(undefined); const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); + function setSchema(schema?: IRSForm) { + if (schema) CalculateStats(schema); + setInnerSchema(schema); + console.log(schema); + } + const fetchData = useCallback( async () => { setError(undefined); - setSchema(undefined); + setInnerSchema(undefined); if (!target) { return; } @@ -19,11 +25,7 @@ export function useRSFormDetails({target}: {target?: string}) { showError: true, setLoading: setLoading, onError: error => setError(error), - onSucccess: (response) => { - CalculateStats(response.data) - console.log(response.data); - setSchema(response.data); - } + onSucccess: (response) => setSchema(response.data) }); }, [target]); @@ -35,5 +37,5 @@ export function useRSFormDetails({target}: {target?: string}) { fetchData(); }, [fetchData]) - return { schema, reload, error, setError, loading }; + return { schema, setSchema, reload, error, setError, loading }; } \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx b/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx index 3231e792..f822c0e6 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ConstituentEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useLayoutEffect, useState } from 'react'; import { useRSForm } from '../../context/RSFormContext'; import { CstType, EditMode, INewCstData } from '../../utils/models'; import { toast } from 'react-toastify'; @@ -10,13 +10,10 @@ import ConstituentsSideList from './ConstituentsSideList'; import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons'; import CreateCstModal from './CreateCstModal'; import { AxiosResponse } from 'axios'; -import { useNavigate } from 'react-router-dom'; -import { RSFormTabsList } from './RSFormTabs'; function ConstituentEditor() { - const navigate = useNavigate(); const { - active, schema, setActive, processing, isEditable, reload, + activeCst, activeID, schema, setActiveID, processing, isEditable, cstDelete, cstUpdate, cstCreate } = useRSForm(); @@ -31,23 +28,23 @@ function ConstituentEditor() { const [convention, setConvention] = useState(''); const [typification, setTypification] = useState('N/A'); - useEffect(() => { + useLayoutEffect(() => { if (schema?.items && schema?.items.length > 0) { - setActive((prev) => (prev || schema?.items![0])); + setActiveID((prev) => (prev || schema?.items![0].id)); } - }, [schema, setActive]) + }, [schema, setActiveID]) - useEffect(() => { - if (active) { - setAlias(active.alias); - setType(getCstTypeLabel(active.cstType)); - setConvention(active.convention || ''); - setTerm(active.term?.raw || ''); - setTextDefinition(active.definition?.text?.raw || ''); - setExpression(active.definition?.formal || ''); - setTypification(active?.parse?.typification || 'N/A'); + useLayoutEffect(() => { + if (activeCst) { + setAlias(activeCst.alias); + setType(getCstTypeLabel(activeCst.cstType)); + setConvention(activeCst.convention || ''); + setTerm(activeCst.term?.raw || ''); + setTextDefinition(activeCst.definition?.text?.raw || ''); + setExpression(activeCst.definition?.formal || ''); + setTypification(activeCst?.parse?.typification || 'N/A'); } - }, [active]); + }, [activeCst]); const handleSubmit = async (event: React.FormEvent) => { @@ -64,37 +61,31 @@ function ConstituentEditor() { 'term': { 'raw': term, 'resolved': '', - 'forms': active?.term?.forms || [], + 'forms': activeCst?.term?.forms || [], } }; - cstUpdate(data) - .then(() => { - toast.success('Изменения сохранены'); - reload(); - }); + cstUpdate(data).then(() => toast.success('Изменения сохранены')); } }; const handleDelete = useCallback( async () => { - if (!active || !window.confirm('Вы уверены, что хотите удалить конституенту?')) { + if (!activeID || !schema?.items || !window.confirm('Вы уверены, что хотите удалить конституенту?')) { return; } const data = { - 'items': [active.entityUID] + 'items': [activeID] } - const index = schema?.items?.indexOf(active) - await cstDelete(data); - if (schema?.items && index && index + 1 < schema?.items?.length) { - setActive(schema?.items[index + 1]); + const index = schema.items.findIndex((cst) => cst.id === activeID); + if (index !== -1 && index + 1 < schema.items.length) { + setActiveID(schema.items[index + 1].id); } - toast.success(`Конституента удалена: ${active.alias}`); - reload(); - }, [active, schema, setActive, cstDelete, reload]); + cstDelete(data).then(() => toast.success('Конституента удалена')); + }, [activeID, schema, setActiveID, cstDelete]); const handleAddNew = useCallback( async (csttype?: CstType) => { - if (!active || !schema) { + if (!activeID || !schema?.items) { return; } if (!csttype) { @@ -103,14 +94,16 @@ function ConstituentEditor() { const data: INewCstData = { 'csttype': csttype, 'alias': createAliasFor(csttype, schema!), - 'insert_after': active.entityUID + 'insert_after': activeID } - cstCreate(data, (response: AxiosResponse) => { - navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${response.data['entityUID']}`); - window.location.reload(); + cstCreate(data, + async (response: AxiosResponse) => { + // navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${response.data['new_cst']['id']}`); + setActiveID(response.data['new_cst']['id']); + toast.success(`Конституента добавлена: ${response.data['new_cst']['alias']}`); }); } - }, [active, schema, cstCreate, navigate]); + }, [activeID, schema, cstCreate, setActiveID]); const handleRename = useCallback(() => { toast.info('Переименование в разработке'); @@ -127,7 +120,7 @@ function ConstituentEditor() { show={showCstModal} toggle={() => setShowCstModal(!showCstModal)} onCreate={handleAddNew} - defaultType={active?.cstType as CstType} + defaultType={activeCst?.cstType as CstType} />
diff --git a/rsconcept/frontend/src/pages/RSFormPage/ConstituentsSideList.tsx b/rsconcept/frontend/src/pages/RSFormPage/ConstituentsSideList.tsx index e99637d7..d391ec74 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ConstituentsSideList.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ConstituentsSideList.tsx @@ -11,7 +11,7 @@ interface ConstituentsSideListProps { } function ConstituentsSideList({expression}: ConstituentsSideListProps) { - const { schema, setActive } = useRSForm(); + const { schema, setActiveID } = useRSForm(); const [filteredData, setFilteredData] = useState(schema?.items || []); const [filterText, setFilterText] = useLocalStorage('side-filter-text', '') const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false); @@ -27,7 +27,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) { if (diff.length > 0) { diff.forEach( (alias, i) => filtered.push({ - entityUID: -i, + id: -i, alias: alias, convention: 'Конституента отсутствует', cstType: CstType.BASE @@ -43,21 +43,21 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) { const handleRowClicked = useCallback( (cst: IConstituenta, event: React.MouseEvent) => { - if (event.altKey && cst.entityUID > 0) { - setActive(cst); + if (event.altKey && cst.id > 0) { + setActiveID(cst.id); } - }, [setActive]); + }, [setActiveID]); const handleDoubleClick = useCallback( (cst: IConstituenta, event: React.MouseEvent) => { - if (cst.entityUID > 0) setActive(cst); - }, [setActive]); + if (cst.id > 0) setActiveID(cst.id); + }, [setActiveID]); const columns = useMemo(() => [ { id: 'id', - selector: (cst: IConstituenta) => cst.entityUID, + selector: (cst: IConstituenta) => cst.id, omit: true, }, { @@ -68,7 +68,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) { maxWidth: '62px', conditionalCellStyles: [ { - when: (cst: IConstituenta) => cst.entityUID <= 0, + when: (cst: IConstituenta) => cst.id <= 0, classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] }, ], @@ -81,7 +81,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) { wrap: true, conditionalCellStyles: [ { - when: (cst: IConstituenta) => cst.entityUID <= 0, + when: (cst: IConstituenta) => cst.id <= 0, classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] }, ], @@ -96,7 +96,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) { wrap: true, conditionalCellStyles: [ { - when: (cst: IConstituenta) => cst.entityUID <= 0, + when: (cst: IConstituenta) => cst.id <= 0, classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]'] }, ], diff --git a/rsconcept/frontend/src/pages/RSFormPage/ConstituentsTable.tsx b/rsconcept/frontend/src/pages/RSFormPage/ConstituentsTable.tsx index 6cb82a39..532daa0a 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ConstituentsTable.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ConstituentsTable.tsx @@ -15,8 +15,11 @@ interface ConstituentsTableProps { } function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { - const { schema, isEditable, cstCreate, cstDelete, reload } = useRSForm(); - const [selected, setSelected] = useState([]); + const { + schema, isEditable, + cstCreate, cstDelete, cstMoveTo + } = useRSForm(); + const [selected, setSelected] = useState([]); const nothingSelected = useMemo(() => selected.length === 0, [selected]); const [showCstModal, setShowCstModal] = useState(false); @@ -28,32 +31,78 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { } }, [onOpenEdit]); + const handleSelectionChange = useCallback( + ({selectedRows}: { + allSelected: boolean; + selectedCount: number; + selectedRows: IConstituenta[]; + }) => { + setSelected(selectedRows.map((cst) => cst.id)); + }, [setSelected]); + + // Delete selected constituents const handleDelete = useCallback(() => { - if (!window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) { + if (!schema?.items || !window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) { return; } const data = { - 'items': selected.map(cst => cst.entityUID) + 'items': selected.map(id => { return {'id': id }; }), } - const deletedNamed = selected.map(cst => cst.alias) - cstDelete(data, (response: AxiosResponse) => { - reload().then(() => toast.success(`Конституенты удалены: ${deletedNamed}`)); - }); - }, [selected, cstDelete, reload]); + const deletedNamed = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias); + cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNamed}`)); + }, [selected, schema?.items, cstDelete]); - const handleMoveUp = useCallback(() => { - toast.info('Перемещение вверх'); - - }, []); + // Move selected cst up + const handleMoveUp = useCallback( + () => { + if (!schema?.items || selected.length === 0) { + return; + } + const currentIndex = schema.items.reduce((prev, cst, index) => { + if (selected.indexOf(cst.id) < 0) { + return prev; + } else if (prev === -1) { + return index; + } + return Math.min(prev, index); + }, -1); + const insertIndex = Math.max(0, currentIndex - 1) + 1 + const data = { + 'items': selected.map(id => { return {'id': id }; }), + 'move_to': insertIndex + } + cstMoveTo(data).then(() => toast.info('Перемещение вверх ' + insertIndex)); + }, [selected, schema?.items, cstMoveTo]); - const handleMoveDown = useCallback(() => { - toast.info('Перемещение вниз'); - }, []); + + // Move selected cst down + const handleMoveDown = useCallback( + async () => { + if (!schema?.items || selected.length === 0) { + return; + } + const currentIndex = schema.items.reduce((prev, cst, index) => { + if (selected.indexOf(cst.id) < 0) { + return prev; + } else if (prev === -1) { + return index; + } + return Math.max(prev, index); + }, -1); + const insertIndex = Math.min(schema.items.length - 1, currentIndex + 1) + 1 + const data = { + 'items': selected.map(id => { return {'id': id }; }), + 'move_to': insertIndex + } + cstMoveTo(data).then(() => toast.info('Перемещение вниз ' + insertIndex)); + }, [selected, schema?.items, cstMoveTo]); + // Generate new names for all constituents const handleReindex = useCallback(() => { toast.info('Переиндексация'); }, []); + // Add new constituent const handleAddNew = useCallback((csttype?: CstType) => { if (!csttype) { setShowCstModal(true); @@ -63,20 +112,34 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { 'alias': createAliasFor(csttype, schema!) } if (selected.length > 0) { - data['insert_after'] = selected[selected.length - 1].entityUID + data['insert_after'] = selected[selected.length - 1] } - cstCreate(data, (response: AxiosResponse) => { - reload().then(() => toast.success(`Добавлена конституента ${response.data['alias']}`)); - }); + cstCreate(data, (response: AxiosResponse) => + toast.success(`Добавлена конституента ${response.data['new_cst']['alias']}`)); } - }, [schema, selected, reload, cstCreate]); + }, [schema, selected, cstCreate]); + + // Implement hotkeys for working with constituents table + const handleTableKey = useCallback((event: React.KeyboardEvent) => { + if (!event.altKey) { + return; + } + if (!isEditable || selected.length === 0) { + return; + } + switch(event.key) { + case 'ArrowUp': handleMoveUp(); return; + case 'ArrowDown': handleMoveDown(); return; + } + console.log(event); + }, [isEditable, selected, handleMoveUp, handleMoveDown]); const columns = useMemo(() => [ { name: 'ID', id: 'id', - selector: (cst: IConstituenta) => cst.entityUID, + selector: (cst: IConstituenta) => cst.id, omit: true, }, { @@ -239,6 +302,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { })}
} +
setSelected(selectedRows)} + onSelectedRowsChange={handleSelectionChange} onRowDoubleClicked={onOpenEdit} onRowClicked={handleRowClicked} dense /> +
); } diff --git a/rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx b/rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx index be078053..b92c2058 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ExpressionEditor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import Button from '../../components/Common/Button'; import Label from '../../components/Common/Label'; import { useRSForm } from '../../context/RSFormContext'; @@ -30,18 +30,18 @@ function ExpressionEditor({ id, label, disabled, isActive, placeholder, value, setValue, toggleEditMode, setTypification, onChange }: ExpressionEditorProps) { - const { schema, active } = useRSForm(); + const { schema, activeCst } = useRSForm(); const [isModified, setIsModified] = useState(false); const { parseData, checkExpression, resetParse, loading } = useCheckExpression({schema: schema}); const expressionCtrl = useRef(null); - useEffect(() => { + useLayoutEffect(() => { setIsModified(false); resetParse(); - }, [active, resetParse]); + }, [activeCst, resetParse]); const handleCheckExpression = useCallback(() => { - const prefix = active?.alias + (active?.cstType === CstType.STRUCTURED ? '::=' : ':=='); + const prefix = activeCst?.alias + (activeCst?.cstType === CstType.STRUCTURED ? '::=' : ':=='); const expression = prefix + value; checkExpression(expression, (response: AxiosResponse) => { // TODO: update cursor position @@ -49,7 +49,7 @@ function ExpressionEditor({ setTypification(response.data['typification']); toast.success('проверка завершена'); }); - }, [value, checkExpression, active, setTypification]); + }, [value, checkExpression, activeCst, setTypification]); const handleEdit = useCallback((id: TokenID, key?: string) => { if (!expressionCtrl.current) { @@ -198,7 +198,7 @@ function ExpressionEditor({
{isActive && }
diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSFormCard.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSFormCard.tsx index 7be7c617..933e01c4 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSFormCard.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSFormCard.tsx @@ -18,7 +18,7 @@ function RSFormCard() { const intl = useIntl(); const { getUserLabel } = useUsers(); const { - schema, update, download, reload, + schema, update, download, isEditable, isOwned, isClaimable, processing, destroy, claim } = useRSForm(); const { user } = useAuth(); @@ -43,10 +43,7 @@ function RSFormCard() { 'comment': comment, 'is_common': common, }; - update(data, () => { - toast.success('Изменения сохранены'); - reload(); - }); + update(data).then(() => toast.success('Изменения сохранены')); }; const handleDelete = @@ -107,7 +104,7 @@ function RSFormCard() { tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' } disabled={!isClaimable || processing || !user} icon={} - onClick={() => claimOwnershipProc(claim, reload)} + onClick={() => claimOwnershipProc(claim)} />