Implement NewConstituenta + add selector component

This commit is contained in:
IRBorisov 2023-07-23 15:23:01 +03:00
parent c9e56e7146
commit e58fd183e9
20 changed files with 479 additions and 143 deletions

View File

@ -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 import apps.rsform.models
from django.conf import settings from django.conf import settings
@ -49,7 +49,6 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': 'Конституета', 'verbose_name': 'Конституета',
'verbose_name_plural': 'Конституенты', 'verbose_name_plural': 'Конституенты',
'unique_together': {('schema', 'alias'), ('schema', 'order')},
}, },
), ),
] ]

View File

@ -1,8 +1,11 @@
import json
from django.db import models, transaction from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from apps.users.models import User from apps.users.models import User
import pyconcept
class CstType(models.TextChoices): class CstType(models.TextChoices):
''' Type of constituenta ''' ''' 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 ''' ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position '''
if position <= 0: if position <= 0:
raise ValidationError('Invalid position: should be positive integer') 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: for cst in update_list:
cst.order += 1 cst.order += 1
cst.save() Constituenta.objects.bulk_update(update_list, ['order'])
return Constituenta.objects.create(
result = Constituenta.objects.create(
schema=self, schema=self,
order=position, order=position,
alias=alias, alias=alias,
csttype=type csttype=type
) )
self._recreate_order()
return Constituenta.objects.get(pk=result.pk)
@transaction.atomic @transaction.atomic
def insert_last(self, alias: str, type: CstType) -> 'Constituenta': def insert_last(self, alias: str, type: CstType) -> 'Constituenta':
''' Insert new constituenta at last position ''' ''' Insert new constituenta at last position '''
position = 1 position = 1
if self.constituents().exists(): if self.constituents().exists():
position += self.constituents().aggregate(models.Max('order'))['order__max'] position += self.constituents().only('order').aggregate(models.Max('order'))['order__max']
return Constituenta.objects.create( result = Constituenta.objects.create(
schema=self, schema=self,
order=position, order=position,
alias=alias, alias=alias,
csttype=type csttype=type
) )
self._recreate_order()
return Constituenta.objects.get(pk=result.pk)
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
@ -111,21 +119,7 @@ class RSForm(models.Model):
comment=data.get('comment', ''), comment=data.get('comment', ''),
is_common=is_common is_common=is_common
) )
order = 1 schema._create_cst_from_json(data['items'])
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
return schema return schema
def to_json(self) -> str: def to_json(self) -> str:
@ -133,7 +127,7 @@ class RSForm(models.Model):
result = self._prepare_json_rsform() result = self._prepare_json_rsform()
items = self.constituents().order_by('order') items = self.constituents().order_by('order')
for cst in items: for cst in items:
result['items'].append(self._prepare_json_cst(cst)) result['items'].append(cst.to_json())
return result return result
def __str__(self): def __str__(self):
@ -148,20 +142,37 @@ class RSForm(models.Model):
'items': [] 'items': []
} }
@staticmethod def _recreate_order(self):
def _prepare_json_cst(cst: 'Constituenta') -> dict: checked = json.loads(pyconcept.check_schema(json.dumps(self.to_json())))
return { update_list = self.constituents().only('id', 'order')
'entityUID': cst.id, if (len(checked['items']) != update_list.count()):
'type': 'constituenta', raise ValidationError
'cstType': cst.csttype, order = 1
'alias': cst.alias, for cst in checked['items']:
'convention': cst.convention, id = cst['entityUID']
'term': cst.term, for oldCst in update_list:
'definition': { if oldCst.id == id:
'formal': cst.definition_formal, oldCst.order = order
'text': cst.definition_text 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): class Constituenta(models.Model):
@ -208,7 +219,20 @@ class Constituenta(models.Model):
class Meta: class Meta:
verbose_name = 'Конституета' verbose_name = 'Конституета'
verbose_name_plural = 'Конституенты' verbose_name_plural = 'Конституенты'
unique_together = (('schema', 'alias'), ('schema', 'order'))
def __str__(self): def __str__(self):
return self.alias 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
}
}

View File

@ -27,3 +27,9 @@ class ConstituentaSerializer(serializers.ModelSerializer):
def update(self, instance: Constituenta, validated_data): def update(self, instance: Constituenta, validated_data):
instance.schema.save() instance.schema.save()
return super().update(instance, validated_data) 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)

View File

@ -40,28 +40,6 @@ class TestConstituenta(TestCase):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', order=1) 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): def test_create_default(self):
cst = Constituenta.objects.create( cst = Constituenta.objects.create(
alias='X1', alias='X1',
@ -158,7 +136,7 @@ class TestRSForm(TestCase):
cst3 = schema.insert_at(4, 'X3', CstType.BASE) cst3 = schema.insert_at(4, 'X3', CstType.BASE)
cst2.refresh_from_db() cst2.refresh_from_db()
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst3.order, 4) self.assertEqual(cst3.order, 3)
self.assertEqual(cst3.schema, schema) self.assertEqual(cst3.schema, schema)
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
@ -169,13 +147,25 @@ class TestRSForm(TestCase):
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst4.order, 3) self.assertEqual(cst4.order, 3)
self.assertEqual(cst4.schema, schema) self.assertEqual(cst4.schema, schema)
self.assertEqual(cst3.order, 5) self.assertEqual(cst3.order, 4)
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
schema.insert_at(0, 'X5', CstType.BASE) 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): def test_insert_last(self):
schema = RSForm.objects.create(title='Test') schema = RSForm.objects.create(title='Test')
cst1 = schema.insert_last('X1', CstType.BASE) cst1 = schema.insert_last('X1', CstType.BASE)

View File

@ -171,6 +171,30 @@ class TestRSFormViewset(APITestCase):
response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/') response = self.client.post(f'/api/rsforms/{self.rsform_owned.id}/claim/')
self.assertEqual(response.status_code, 403) 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): class TestFunctionalViews(APITestCase):
def setUp(self): def setUp(self):

View File

@ -41,7 +41,8 @@ class RSFormViewSet(viewsets.ModelViewSet):
return serializer.save() return serializer.save()
def get_permissions(self): 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] permission_classes = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'claim']: elif self.action in ['create', 'claim']:
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@ -49,6 +50,21 @@ class RSFormViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes] 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']) @action(detail=True, methods=['post'])
def claim(self, request, pk=None): def claim(self, request, pk=None):
schema: models.RSForm = self.get_object() schema: models.RSForm = self.get_object()

View File

@ -8,9 +8,8 @@ from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), 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('api/', include('apps.rsform.urls')),
path('users/', include('apps.users.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) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -25,6 +25,7 @@
"react-loader-spinner": "^5.3.4", "react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.12.1", "react-router-dom": "^6.12.1",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-select": "^5.7.4",
"react-tabs": "^6.0.1", "react-tabs": "^6.0.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"styled-components": "^6.0.4", "styled-components": "^6.0.4",
@ -2369,6 +2370,70 @@
"postcss-selector-parser": "^6.0.10" "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": { "node_modules/@emotion/is-prop-valid": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" "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": { "node_modules/@emotion/stylis": {
"version": "0.8.5", "version": "0.8.5",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", "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", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" "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": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "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": "^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": { "node_modules/@formatjs/ecma402-abstract": {
"version": "1.17.0", "version": "1.17.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz",
@ -4330,6 +4466,14 @@
"@types/react": "*" "@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": { "node_modules/@types/resolve": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -7008,6 +7152,15 @@
"utila": "~0.4" "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": { "node_modules/dom-serializer": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "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" "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": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -12261,6 +12419,11 @@
"node": ">= 4.0.0" "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": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "node_modules/react-tabs": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.1.tgz", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.1.tgz",
@ -14999,6 +15182,21 @@
"react-dom": ">=16" "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": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -16923,6 +17121,19 @@
"requires-port": "^1.0.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -20,6 +20,7 @@
"react-loader-spinner": "^5.3.4", "react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.12.1", "react-router-dom": "^6.12.1",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-select": "^5.7.4",
"react-tabs": "^6.0.1", "react-tabs": "^6.0.1",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"styled-components": "^6.0.4", "styled-components": "^6.0.4",

View File

@ -8,35 +8,38 @@ interface BackendErrorProps {
} }
function DescribeError(error: ErrorInfo) { function DescribeError(error: ErrorInfo) {
console.log(error);
if (!error) { if (!error) {
return <p>Ошибки отсутствуют</p>; return <p>Ошибки отсутствуют</p>;
} else if (typeof error === 'string') { } else if (typeof error === 'string') {
return <p>{error}</p>; return <p>{error}</p>;
} else if (axios.isAxiosError(error)) { } else if (!axios.isAxiosError(error)) {
if (!error?.response) {
return <p>Нет ответа от сервера</p>;
}
if (error.response.status === 404) {
return (
<div className='flex flex-col justify-start'>
<p>{`Обращение к несуществующему API`}</p>
<PrettyJson data={error} />
</div>
);
}
return (
<div className='flex flex-col justify-start'>
<p className='underline'>Ошибка</p>
<p>{error.message}</p>
{error.response.data && (<>
<p className='mt-2 underline'>Описание</p>
<PrettyJson data={error.response.data} />
</>)}
</div>
);
} else {
return <PrettyJson data={error} />; return <PrettyJson data={error} />;
} }
if (!error?.response) {
return <p>Нет ответа от сервера</p>;
}
if (error.response.status === 404) {
return (
<div className='flex flex-col justify-start'>
<p>{`Обращение к несуществующему API`}</p>
<PrettyJson data={error} />
</div>
);
}
const isHtml = error.response.headers['content-type'].includes('text/html');
return (
<div className='flex flex-col justify-start'>
<p className='underline'>Ошибка</p>
<p>{error.message}</p>
{error.response.data && (<>
<p className='mt-2 underline'>Описание</p>
{ isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data }} /> }
{ !isHtml && <PrettyJson data={error.response.data} />}
</>)}
</div>
);
} }
function BackendError({error}: BackendErrorProps) { function BackendError({error}: BackendErrorProps) {

View File

@ -26,6 +26,11 @@ function Modal({title, show, toggle, onSubmit, onCancel, canSubmit, children, su
if(onCancel) onCancel(); if(onCancel) onCancel();
}; };
const handleSubmit = () => {
toggle();
onSubmit();
};
return ( return (
<> <>
<div className='fixed top-0 left-0 w-full h-full clr-modal opacity-50 z-50'> <div className='fixed top-0 left-0 w-full h-full clr-modal opacity-50 z-50'>
@ -41,7 +46,7 @@ function Modal({title, show, toggle, onSubmit, onCancel, canSubmit, children, su
widthClass='min-w-[6rem] w-fit h-fit' widthClass='min-w-[6rem] w-fit h-fit'
colorClass='clr-btn-primary' colorClass='clr-btn-primary'
disabled={!canSubmit} disabled={!canSubmit}
onClick={onSubmit} onClick={handleSubmit}
/> />
<Button <Button
text='Отмена' text='Отмена'

View File

@ -1,21 +1,16 @@
import { InputHTMLAttributes } from 'react';
import Label from './Label'; import Label from './Label';
interface TextInputProps { interface TextInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
id: string id: string
type: string
label: string label: string
required?: boolean
disabled?: boolean
placeholder?: string
widthClass?: string widthClass?: string
value?: any
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
onFocus?: () => void
} }
function TextInput({ function TextInput({
id, type, required, label, disabled, placeholder, widthClass='w-full', value, id, required, label, widthClass='w-full',
onChange, onFocus ...props
}: TextInputProps) { }: TextInputProps) {
return ( return (
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'> <div className='flex flex-col items-start [&:not(:first-child)]:mt-3'>
@ -27,12 +22,7 @@ function TextInput({
<input id={id} <input id={id}
className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 truncate hover:text-clip '+ widthClass} className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 truncate hover:text-clip '+ widthClass}
required={required} required={required}
type={type} {...props}
placeholder={placeholder}
value={value}
onChange={onChange}
onFocus={onFocus}
disabled={disabled}
/> />
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { IConstituenta, IRSForm } from '../utils/models';
import { useRSFormDetails } from '../hooks/useRSFormDetails'; import { useRSFormDetails } from '../hooks/useRSFormDetails';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm } from '../utils/backendAPI'; import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm, postNewConstituenta } from '../utils/backendAPI';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
interface IRSFormContext { interface IRSFormContext {
@ -30,6 +30,7 @@ interface IRSFormContext {
download: (callback: BackendCallback) => void download: (callback: BackendCallback) => void
cstUpdate: (data: any, callback: BackendCallback) => void cstUpdate: (data: any, callback: BackendCallback) => void
cstCreate: (data: any, callback: BackendCallback) => void
} }
export const RSFormContext = createContext<IRSFormContext>({ export const RSFormContext = createContext<IRSFormContext>({
@ -56,16 +57,17 @@ export const RSFormContext = createContext<IRSFormContext>({
download: () => {}, download: () => {},
cstUpdate: () => {}, cstUpdate: () => {},
cstCreate: () => {},
}) })
interface RSFormStateProps { interface RSFormStateProps {
id: string schemaID: string
children: React.ReactNode children: React.ReactNode
} }
export const RSFormState = ({ id, children }: RSFormStateProps) => { export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const { user } = useAuth(); const { user } = useAuth();
const { schema, reload, error, setError, loading } = useRSFormDetails({target: id}); const { schema, reload, error, setError, loading } = useRSFormDetails({target: schemaID});
const [processing, setProcessing] = useState(false) const [processing, setProcessing] = useState(false)
const [active, setActive] = useState<IConstituenta | undefined>(undefined); const [active, setActive] = useState<IConstituenta | undefined>(undefined);
@ -91,7 +93,7 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
async function update(data: any, callback?: BackendCallback) { async function update(data: any, callback?: BackendCallback) {
setError(undefined); setError(undefined);
patchRSForm(id, { patchRSForm(schemaID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
@ -102,7 +104,7 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
async function destroy(callback: BackendCallback) { async function destroy(callback: BackendCallback) {
setError(undefined); setError(undefined);
deleteRSForm(id, { deleteRSForm(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
@ -112,7 +114,7 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
async function claim(callback: BackendCallback) { async function claim(callback: BackendCallback) {
setError(undefined); setError(undefined);
postClaimRSForm(id, { postClaimRSForm(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
@ -122,7 +124,7 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
async function download(callback: BackendCallback) { async function download(callback: BackendCallback) {
setError(undefined); setError(undefined);
getTRSFile(id, { getTRSFile(schemaID, {
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => setError(error), onError: error => setError(error),
@ -141,6 +143,17 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
}); });
} }
async function cstCreate(data: any, callback?: BackendCallback) {
setError(undefined);
postNewConstituenta(schemaID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSucccess: callback
});
}
return ( return (
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{
schema, error, loading, processing, schema, error, loading, processing,
@ -150,8 +163,8 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
toggleReadonly: () => setReadonly(prev => !prev), toggleReadonly: () => setReadonly(prev => !prev),
isOwned, isEditable, isClaimable, isOwned, isEditable, isClaimable,
isTracking, toggleTracking, isTracking, toggleTracking,
cstUpdate, reload, update, download, destroy, claim,
reload, update, download, destroy, claim cstUpdate, cstCreate
}}> }}>
{ children } { children }
</RSFormContext.Provider> </RSFormContext.Provider>

View File

@ -44,6 +44,7 @@ function LoginPage() {
required required
type='text' type='text'
value={username} value={username}
autoFocus
onChange={event => setUsername(event.target.value)} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' <TextInput id='password'

View File

@ -1,29 +1,25 @@
import { CstType, IConstituenta, ParsingStatus, ValueClass, inferStatus } from '../../utils/models' import { CstType, IConstituenta, INewCstData, ParsingStatus, ValueClass, inferStatus } from '../../utils/models'
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import DataTableThemed, { SelectionInfo } from '../../components/Common/DataTableThemed'; import DataTableThemed from '../../components/Common/DataTableThemed';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import { ArrowDownIcon, ArrowUpIcon, ArrowsRotateIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons'; import { ArrowDownIcon, ArrowUpIcon, ArrowsRotateIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Divider from '../../components/Common/Divider'; import Divider from '../../components/Common/Divider';
import { getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI'; import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
import CreateCstModal from './CreateCstModal'; import CreateCstModal from './CreateCstModal';
import { AxiosResponse } from 'axios';
interface ConstituentsTableProps { interface ConstituentsTableProps {
onOpenEdit: (cst: IConstituenta) => void onOpenEdit: (cst: IConstituenta) => void
} }
function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
const { schema, isEditable, } = useRSForm(); const { schema, isEditable, cstCreate, reload } = useRSForm();
const [selectedRows, setSelectedRows] = useState<IConstituenta[]>([]); const [selectedRows, setSelectedRows] = useState<IConstituenta[]>([]);
const nothingSelected = useMemo(() => selectedRows.length === 0, [selectedRows]); const nothingSelected = useMemo(() => selectedRows.length === 0, [selectedRows]);
const [showCstModal, setShowCstModal] = useState(true); const [showCstModal, setShowCstModal] = useState(false);
const handleRowSelected = useCallback(
({selectedRows} : SelectionInfo<IConstituenta>) => {
setSelectedRows(selectedRows);
}, []);
const handleRowClicked = useCallback( const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => { (cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
@ -48,13 +44,23 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
toast.info('Переиндексация'); toast.info('Переиндексация');
}, []); }, []);
const handleAddNew = useCallback((cstType?: CstType) => { const handleAddNew = useCallback((csttype?: CstType) => {
if (!cstType) { if (!csttype) {
setShowCstModal(true); setShowCstModal(true);
} else { } else {
toast.info(`Новая конституента ${cstType || 'NEW'}`); let data: INewCstData = {
csttype: csttype,
alias: createAliasFor(csttype, schema!)
}
if (selectedRows.length > 0) {
data['insert_after'] = selectedRows[selectedRows.length - 1].entityUID
}
cstCreate(data, (response: AxiosResponse) => {
reload();
toast.info(`Добавлена конституента ${response.data['alias']}`);
});
} }
}, []); }, [schema, selectedRows, reload, cstCreate]);
const columns = useMemo(() => const columns = useMemo(() =>
[ [
@ -228,19 +234,20 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
data={schema!.items!} data={schema!.items!}
columns={columns} columns={columns}
keyField='id' keyField='id'
noDataComponent={<span className='p-2 flex flex-col justify-center text-center'> noDataComponent={
<p>Список пуст</p> <span className='flex flex-col justify-center p-2 text-center'>
<p>Создайте новую конституенту</p> <p>Список пуст</p>
</span>} <p>Создайте новую конституенту</p>
</span>
}
striped striped
highlightOnHover highlightOnHover
pointerOnHover pointerOnHover
selectableRows selectableRows
// selectableRowSelected={(cst) => selectedRows.indexOf(cst) < -1}
selectableRowsHighlight selectableRowsHighlight
onSelectedRowsChange={handleRowSelected} onSelectedRowsChange={({selectedRows}) => setSelectedRows(selectedRows)}
onRowDoubleClicked={onOpenEdit} onRowDoubleClicked={onOpenEdit}
onRowClicked={handleRowClicked} onRowClicked={handleRowClicked}
dense dense

View File

@ -1,7 +1,8 @@
import { toast } from 'react-toastify';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import { CstType } from '../../utils/models'; import { CstType } from '../../utils/models';
import { useState } from 'react'; import Select from 'react-select';
import { CstTypeSelector } from '../../utils/staticUI';
import { useEffect, useState } from 'react';
interface CreateCstModalProps { interface CreateCstModalProps {
show: boolean show: boolean
@ -11,11 +12,17 @@ interface CreateCstModalProps {
function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) { function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [selectedType, setSelectedType] = useState<CstType|undefined>(undefined);
const handleSubmit = () => { const handleSubmit = () => {
toast.info('Создание конституент'); if (selectedType) onCreate(selectedType);
}; };
useEffect(() => {
setValidated(selectedType !== undefined);
}, [selectedType]
);
return ( return (
<Modal <Modal
title='Создание конституенты' title='Создание конституенты'
@ -24,8 +31,11 @@ function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<p>Выбор типа конституенты</p> <Select
<p>Добавить после выбранной или в конец</p> options={CstTypeSelector}
placeholder='Выберите тип'
onChange={(data) => setSelectedType(data?.value)}
/>
</Modal> </Modal>
) )
} }

View File

@ -5,7 +5,7 @@ import RSFormTabs from './RSFormTabs';
function RSFormPage() { function RSFormPage() {
const { id } = useParams(); const { id } = useParams();
return ( return (
<RSFormState id={id || ''}> <RSFormState schemaID={id || ''}>
<RSFormTabs /> <RSFormTabs />
</RSFormState> </RSFormState>
); );

View File

@ -150,6 +150,14 @@ export async function postCheckExpression(schema: string, request?: IFrontReques
}); });
} }
export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data['alias']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/new-constituenta/`,
request: request
});
}
// ====== Helper functions =========== // ====== Helper functions ===========
function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) { function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {

View File

@ -32,6 +32,13 @@ export interface IUserSignupData {
password2: string password2: string
} }
// User data for signup
export interface INewCstData {
alias: string
csttype: CstType
insert_after?: number
}
// Constituenta type // Constituenta type
export enum CstType { export enum CstType {
BASE = 'basic', BASE = 'basic',

View File

@ -1,4 +1,4 @@
import { CstType, ExpressionStatus, IConstituenta, ParsingStatus, TokenID } from './models'; import { CstType, ExpressionStatus, IConstituenta, IRSForm, ParsingStatus, TokenID } from './models';
export interface IRSButtonData { export interface IRSButtonData {
text: string text: string
@ -195,6 +195,12 @@ export function getCstTypeLabel(type: CstType) {
} }
} }
export const CstTypeSelector = (Object.values(CstType)).map(
(typeStr) => {
const type = typeStr as CstType;
return {value: type, label: getCstTypeLabel(type)};
});
export function getCstTypePrefix(type: CstType) { export function getCstTypePrefix(type: CstType) {
switch(type) { switch(type) {
case CstType.BASE: return 'X'; case CstType.BASE: return 'X';
@ -251,3 +257,19 @@ export function getStatusInfo(status?: ExpressionStatus): IStatusInfo {
export function extractGlobals(expression: string): Set<string> { export function extractGlobals(expression: string): Set<string> {
return new Set(expression.match(/[XCSADFPT]\d+/g) || []); return new Set(expression.match(/[XCSADFPT]\d+/g) || []);
} }
export function createAliasFor(type: CstType, schema: IRSForm): string {
let index = 1;
let prefix = getCstTypePrefix(type);
let name = prefix + index;
if (schema.items && schema.items.length > 0) {
for (let i = 0; i < schema.items.length; ++i) {
if (schema.items[i].alias === name) {
++index;
name = prefix + index;
i = 0;
}
}
}
return name;
}