diff --git a/rsconcept/backend/apps/rsform/migrations/0001_initial.py b/rsconcept/backend/apps/rsform/migrations/0001_initial.py index 0dbc53d3..8678babf 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.1 on 2023-05-18 18:00 +# Generated by Django 4.2.3 on 2023-07-23 11:55 import apps.rsform.models from django.conf import settings @@ -49,7 +49,6 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'Конституета', 'verbose_name_plural': 'Конституенты', - 'unique_together': {('schema', 'alias'), ('schema', 'order')}, }, ), ] diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 7eb4ecde..e47dd36e 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -1,8 +1,11 @@ +import json from django.db import models, transaction from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from apps.users.models import User +import pyconcept + class CstType(models.TextChoices): ''' Type of constituenta ''' @@ -77,29 +80,34 @@ class RSForm(models.Model): ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' if position <= 0: raise ValidationError('Invalid position: should be positive integer') - update_list = Constituenta.objects.filter(schema=self, order__gte=position) + update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self, order__gte=position) for cst in update_list: cst.order += 1 - cst.save() - return Constituenta.objects.create( + Constituenta.objects.bulk_update(update_list, ['order']) + + result = Constituenta.objects.create( schema=self, order=position, alias=alias, csttype=type ) + self._recreate_order() + return Constituenta.objects.get(pk=result.pk) @transaction.atomic def insert_last(self, alias: str, type: CstType) -> 'Constituenta': ''' Insert new constituenta at last position ''' position = 1 if self.constituents().exists(): - position += self.constituents().aggregate(models.Max('order'))['order__max'] - return Constituenta.objects.create( + position += self.constituents().only('order').aggregate(models.Max('order'))['order__max'] + result = Constituenta.objects.create( schema=self, order=position, alias=alias, csttype=type ) + self._recreate_order() + return Constituenta.objects.get(pk=result.pk) @staticmethod @transaction.atomic @@ -111,21 +119,7 @@ class RSForm(models.Model): comment=data.get('comment', ''), is_common=is_common ) - order = 1 - for cst in data['items']: - # TODO: get rid of empty_term etc. Use None instead - Constituenta.objects.create( - alias=cst['alias'], - schema=schema, - 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 - ) - order += 1 + schema._create_cst_from_json(data['items']) return schema def to_json(self) -> str: @@ -133,7 +127,7 @@ class RSForm(models.Model): result = self._prepare_json_rsform() items = self.constituents().order_by('order') for cst in items: - result['items'].append(self._prepare_json_cst(cst)) + result['items'].append(cst.to_json()) return result def __str__(self): @@ -148,20 +142,37 @@ class RSForm(models.Model): 'items': [] } - @staticmethod - def _prepare_json_cst(cst: 'Constituenta') -> dict: - return { - 'entityUID': cst.id, - 'type': 'constituenta', - 'cstType': cst.csttype, - 'alias': cst.alias, - 'convention': cst.convention, - 'term': cst.term, - 'definition': { - 'formal': cst.definition_formal, - 'text': cst.definition_text - } - } + def _recreate_order(self): + 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()): + raise ValidationError + order = 1 + for cst in checked['items']: + id = cst['entityUID'] + for oldCst in update_list: + if oldCst.id == id: + oldCst.order = order + order += 1 + break + Constituenta.objects.bulk_update(update_list, ['order']) + + 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 + ) + order += 1 class Constituenta(models.Model): @@ -208,7 +219,20 @@ class Constituenta(models.Model): class Meta: verbose_name = 'Конституета' verbose_name_plural = 'Конституенты' - unique_together = (('schema', 'alias'), ('schema', 'order')) def __str__(self): return self.alias + + def to_json(self) -> str: + return { + 'entityUID': self.id, + 'type': 'constituenta', + 'cstType': self.csttype, + 'alias': self.alias, + 'convention': self.convention, + 'term': self.term, + 'definition': { + 'formal': self.definition_formal, + 'text': self.definition_text + } + } diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index ac5dc180..139f4b2b 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -27,3 +27,9 @@ class ConstituentaSerializer(serializers.ModelSerializer): def update(self, instance: Constituenta, validated_data): instance.schema.save() return super().update(instance, validated_data) + + +class NewConstituentaSerializer(serializers.Serializer): + alias = serializers.CharField(max_length=8) + csttype = serializers.CharField(max_length=10) + insert_after = serializers.IntegerField(required=False) diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index d0df3ed9..41bc773d 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -40,28 +40,6 @@ class TestConstituenta(TestCase): with self.assertRaises(IntegrityError): Constituenta.objects.create(alias='X1', order=1) - def test_alias_unique(self): - alias = 'X1' - - original = Constituenta.objects.create(alias=alias, order=1, schema=self.schema1) - self.assertIsNotNone(original) - - clone = Constituenta.objects.create(alias=alias, order=2, schema=self.schema2) - self.assertNotEqual(clone, original) - - with self.assertRaises(IntegrityError): - Constituenta.objects.create(alias=alias, order=1, schema=self.schema1) - - def test_order_unique(self): - original = Constituenta.objects.create(alias='X1', order=1, schema=self.schema1) - self.assertIsNotNone(original) - - clone = Constituenta.objects.create(alias='X2', order=1, schema=self.schema2) - self.assertNotEqual(clone, original) - - with self.assertRaises(IntegrityError): - Constituenta.objects.create(alias='X2', order=1, schema=self.schema1) - def test_create_default(self): cst = Constituenta.objects.create( alias='X1', @@ -158,7 +136,7 @@ class TestRSForm(TestCase): cst3 = schema.insert_at(4, 'X3', CstType.BASE) cst2.refresh_from_db() cst1.refresh_from_db() - self.assertEqual(cst3.order, 4) + self.assertEqual(cst3.order, 3) self.assertEqual(cst3.schema, schema) self.assertEqual(cst2.order, 1) self.assertEqual(cst1.order, 2) @@ -169,13 +147,25 @@ class TestRSForm(TestCase): cst1.refresh_from_db() self.assertEqual(cst4.order, 3) self.assertEqual(cst4.schema, schema) - self.assertEqual(cst3.order, 5) + self.assertEqual(cst3.order, 4) self.assertEqual(cst2.order, 1) self.assertEqual(cst1.order, 2) with self.assertRaises(ValidationError): schema.insert_at(0, 'X5', CstType.BASE) + def test_insert_at_reorder(self): + schema = RSForm.objects.create(title='Test') + schema.insert_at(1, 'X1', CstType.BASE) + d1 = schema.insert_at(2, 'D1', CstType.TERM) + d2 = schema.insert_at(1, 'D2', CstType.TERM) + d1.refresh_from_db() + self.assertEqual(d1.order, 3) + self.assertEqual(d2.order, 2) + + x2 = schema.insert_at(4, 'X2', CstType.BASE) + self.assertEqual(x2.order, 2) + def test_insert_last(self): schema = RSForm.objects.create(title='Test') cst1 = schema.insert_last('X1', CstType.BASE) diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 4279ee01..072bcdd0 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -171,6 +171,30 @@ class TestRSFormViewset(APITestCase): response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/') self.assertEqual(response.status_code, 403) + def test_create_constituenta(self): + data = json.dumps({'alias': 'X3', 'csttype': 'basic'}) + response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/new-constituenta/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 403) + + schema = self.rsform_owned + Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1) + x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2) + response = self.client.post(f'/api/rsforms/{schema.id}/new-constituenta/', + 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(x3.order, 3) + + data = json.dumps({'alias': 'X4', 'csttype': 'basic', 'insert_after': x2.id}) + response = self.client.post(f'/api/rsforms/{schema.id}/new-constituenta/', + 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(x4.order, 3) + class TestFunctionalViews(APITestCase): def setUp(self): diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 5319a5cb..15c2c20a 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -41,7 +41,8 @@ class RSFormViewSet(viewsets.ModelViewSet): return serializer.save() def get_permissions(self): - if self.action in ['update', 'destroy', 'partial_update']: + if self.action in ['update', 'destroy', 'partial_update', + 'new_constituenta']: permission_classes = [utils.ObjectOwnerOrAdmin] elif self.action in ['create', 'claim']: permission_classes = [permissions.IsAuthenticated] @@ -49,6 +50,21 @@ class RSFormViewSet(viewsets.ModelViewSet): permission_classes = [permissions.AllowAny] return [permission() for permission in permission_classes] + @action(detail=True, methods=['post'], url_path='new-constituenta') + def new_constituenta(self, request, pk): + ''' View schema contents (including constituents) ''' + schema: models.RSForm = self.get_object() + serializer = serializers.NewConstituentaSerializer(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']) + constituenta = schema.insert_at(cstafter.order + 1, + serializer.validated_data['alias'], + 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()) + @action(detail=True, methods=['post']) def claim(self, request, pk=None): schema: models.RSForm = self.get_object() diff --git a/rsconcept/backend/project/urls.py b/rsconcept/backend/project/urls.py index ca02a98b..204b737d 100644 --- a/rsconcept/backend/project/urls.py +++ b/rsconcept/backend/project/urls.py @@ -8,9 +8,8 @@ from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), - path('', lambda request: redirect('docs/', permanent=True)), - path('docs/', include_docs_urls(title='ConceptPortal API'), - name='docs'), path('api/', include('apps.rsform.urls')), path('users/', include('apps.users.urls')), + path('docs/', include_docs_urls(title='ConceptPortal API'), name='docs'), + path('', lambda request: redirect('docs/', permanent=True)), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index 4d5709a1..f642658a 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -25,6 +25,7 @@ "react-loader-spinner": "^5.3.4", "react-router-dom": "^6.12.1", "react-scripts": "^5.0.1", + "react-select": "^5.7.4", "react-tabs": "^6.0.1", "react-toastify": "^9.1.3", "styled-components": "^6.0.4", @@ -2369,6 +2370,70 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", @@ -2382,6 +2447,46 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, + "node_modules/@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, "node_modules/@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -2392,6 +2497,24 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2485,6 +2608,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.5.tgz", + "integrity": "sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==", + "dependencies": { + "@floating-ui/core": "^1.3.1" + } + }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", @@ -4330,6 +4466,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -7008,6 +7152,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8316,6 +8469,11 @@ "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -12261,6 +12419,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -14975,6 +15138,26 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/react-select": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.4.tgz", + "integrity": "sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-tabs": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.1.tgz", @@ -14999,6 +15182,21 @@ "react-dom": ">=16" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16923,6 +17121,19 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index 12b0ca5b..1bf47974 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -20,6 +20,7 @@ "react-loader-spinner": "^5.3.4", "react-router-dom": "^6.12.1", "react-scripts": "^5.0.1", + "react-select": "^5.7.4", "react-tabs": "^6.0.1", "react-toastify": "^9.1.3", "styled-components": "^6.0.4", diff --git a/rsconcept/frontend/src/components/BackendError.tsx b/rsconcept/frontend/src/components/BackendError.tsx index 36dba412..2dd66e88 100644 --- a/rsconcept/frontend/src/components/BackendError.tsx +++ b/rsconcept/frontend/src/components/BackendError.tsx @@ -8,35 +8,38 @@ interface BackendErrorProps { } function DescribeError(error: ErrorInfo) { + console.log(error); if (!error) { return

Ошибки отсутствуют

; } else if (typeof error === 'string') { return

{error}

; - } else if (axios.isAxiosError(error)) { - if (!error?.response) { - return

Нет ответа от сервера

; - } - if (error.response.status === 404) { - return ( -
-

{`Обращение к несуществующему API`}

- -
- ); - } - return ( -
-

Ошибка

-

{error.message}

- {error.response.data && (<> -

Описание

- - )} -
- ); - } else { + } else if (!axios.isAxiosError(error)) { return ; } + if (!error?.response) { + return

Нет ответа от сервера

; + } + if (error.response.status === 404) { + return ( +
+

{`Обращение к несуществующему API`}

+ +
+ ); + } + + const isHtml = error.response.headers['content-type'].includes('text/html'); + return ( +
+

Ошибка

+

{error.message}

+ {error.response.data && (<> +

Описание

+ { isHtml &&
} + { !isHtml && } + )} +
+ ); } function BackendError({error}: BackendErrorProps) { diff --git a/rsconcept/frontend/src/components/Common/Modal.tsx b/rsconcept/frontend/src/components/Common/Modal.tsx index d07b7d8e..300cc7e7 100644 --- a/rsconcept/frontend/src/components/Common/Modal.tsx +++ b/rsconcept/frontend/src/components/Common/Modal.tsx @@ -26,6 +26,11 @@ function Modal({title, show, toggle, onSubmit, onCancel, canSubmit, children, su if(onCancel) onCancel(); }; + const handleSubmit = () => { + toggle(); + onSubmit(); + }; + return ( <>
@@ -41,7 +46,7 @@ function Modal({title, show, toggle, onSubmit, onCancel, canSubmit, children, su widthClass='min-w-[6rem] w-fit h-fit' colorClass='clr-btn-primary' disabled={!canSubmit} - onClick={onSubmit} + onClick={handleSubmit} />