Refactor backend-frontend interaction and add MoveCst

some bugs are still present in MoveCst and CreateCst
This commit is contained in:
IRBorisov 2023-07-24 22:34:03 +03:00
parent ffbeafc3f5
commit 920a7baff4
21 changed files with 536 additions and 298 deletions

View File

@ -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={

View File

@ -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,
},
},
}

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -7,7 +7,7 @@ rsform_router = routers.SimpleRouter()
rsform_router.register(r'rsforms', views.RSFormViewSet)
urlpatterns = [
path('constituents/<int:pk>/', views.ConstituentAPIView.as_view()),
path('constituents/<int:pk>/', 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),

View File

@ -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):

View File

@ -23,7 +23,6 @@ function App() {
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
limit={5}
/>
<main className='min-h-[calc(100vh-7.5rem)] px-2 h-fit'>
<Routes>

View File

@ -4,10 +4,7 @@ import type { TabProps } from 'react-tabs';
function ConceptTab({children, className, ...otherProps} : TabProps) {
return (
<Tab
className={
'px-2 py-1 text-sm hover:cursor-pointer clr-tab'
+ ' ' + className
}
className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab ${className} whitespace-nowrap`}
{...otherProps}
>
{children}

View File

@ -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<React.SetStateAction<IConstituenta | undefined>>
setActiveID: React.Dispatch<React.SetStateAction<number | undefined>>
toggleForceAdmin: () => void
toggleReadonly: () => void
toggleTracking: () => void
reload: () => Promise<void>
update: (data: any, callback?: BackendCallback) => Promise<void>
destroy: (callback?: BackendCallback) => Promise<void>
claim: (callback?: BackendCallback) => Promise<void>
@ -33,36 +39,19 @@ interface IRSFormContext {
cstUpdate: (data: any, callback?: BackendCallback) => Promise<void>
cstCreate: (data: any, callback?: BackendCallback) => Promise<void>
cstDelete: (data: any, callback?: BackendCallback) => Promise<void>
cstMoveTo: (data: any, callback?: BackendCallback) => Promise<void>
}
export const RSFormContext = createContext<IRSFormContext>({
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<IRSFormContext | null>(null);
export const useRSForm = () => {
const context = useContext(RSFormContext);
if (!context) {
throw new Error(
'useRSForm has to be used within <RSFormState.Provider>'
);
}
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<IConstituenta | undefined>(undefined);
const [activeID, setActiveID] = useState<number | undefined>(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 (
<RSFormContext.Provider value={{
schema, error, loading, processing,
active, setActive,
forceAdmin, readonly,
toggleForceAdmin: () => 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 }
</RSFormContext.Provider>
);
}
export const useRSForm = () => useContext(RSFormContext);
}

View File

@ -4,14 +4,20 @@ import { ErrorInfo } from '../components/BackendError';
import { getRSFormDetails } from '../utils/backendAPI';
export function useRSFormDetails({target}: {target?: string}) {
const [schema, setSchema] = useState<IRSForm | undefined>();
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(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 };
}

View File

@ -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<HTMLFormElement>) => {
@ -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}
/>
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min px-4 py-2 border'>
<div className='flex items-start justify-between'>

View File

@ -11,7 +11,7 @@ interface ConstituentsSideListProps {
}
function ConstituentsSideList({expression}: ConstituentsSideListProps) {
const { schema, setActive } = useRSForm();
const { schema, setActiveID } = useRSForm();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(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<Element, 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<Element, 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]']
},
],

View File

@ -15,8 +15,11 @@ interface ConstituentsTableProps {
}
function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
const { schema, isEditable, cstCreate, cstDelete, reload } = useRSForm();
const [selected, setSelected] = useState<IConstituenta[]>([]);
const {
schema, isEditable,
cstCreate, cstDelete, cstMoveTo
} = useRSForm();
const [selected, setSelected] = useState<number[]>([]);
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<HTMLDivElement>) => {
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) {
})}
</div>}
</div>
<div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}>
<DataTableThemed
data={schema!.items!}
columns={columns}
@ -256,11 +320,12 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
selectableRows
selectableRowsHighlight
onSelectedRowsChange={({selectedRows}) => setSelected(selectedRows)}
onSelectedRowsChange={handleSelectionChange}
onRowDoubleClicked={onOpenEdit}
onRowClicked={handleRowClicked}
dense
/>
</div>
</div>
</>);
}

View File

@ -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<HTMLTextAreaElement>(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({
<div className='flex flex-col gap-2'>
{isActive && <StatusBar
isModified={isModified}
constituenta={active}
constituenta={activeCst}
parseData={parseData}
/>}
<Button
@ -210,7 +210,7 @@ function ExpressionEditor({
{isActive && EditButtons}
{!isActive && <StatusBar
isModified={isModified}
constituenta={active}
constituenta={activeCst}
parseData={parseData}
/>}
</div>

View File

@ -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={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
onClick={() => claimOwnershipProc(claim, reload)}
onClick={() => claimOwnershipProc(claim)}
/>
<Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}

View File

@ -2,7 +2,7 @@ import { Tabs, TabList, TabPanel } from 'react-tabs';
import ConstituentsTable from './ConstituentsTable';
import { IConstituenta } from '../../utils/models';
import { useRSForm } from '../../context/RSFormContext';
import { useEffect, useState } from 'react';
import { useEffect, useLayoutEffect, useState } from 'react';
import ConceptTab from '../../components/Common/ConceptTab';
import RSFormCard from './RSFormCard';
import { Loader } from '../../components/Common/Loader';
@ -19,13 +19,13 @@ export enum RSFormTabsList {
}
function RSFormTabs() {
const { setActive, active, error, schema, loading } = useRSForm();
const { setActiveID, activeCst, activeID, error, schema, loading } = useRSForm();
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD);
const [init, setInit] = useState(false);
const onEditCst = (cst: IConstituenta) => {
console.log(`Set active cst: ${cst.alias}`);
setActive(cst);
setActiveID(cst.id);
setTabIndex(RSFormTabsList.CST_EDIT)
};
@ -33,15 +33,15 @@ function RSFormTabs() {
setTabIndex(index);
};
useEffect(() => {
useLayoutEffect(() => {
if (schema) {
const url = new URL(window.location.href);
const activeQuery = url.searchParams.get('active');
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
setActive(activeCst);
const activeCst = schema?.items?.find((cst) => cst.id === Number(activeQuery)) || undefined;
setActiveID(activeCst?.id);
setInit(true);
}
}, [setActive, schema, setInit]);
}, [setActiveID, schema, setInit]);
useEffect(() => {
const url = new URL(window.location.href);
@ -54,13 +54,13 @@ function RSFormTabs() {
const url = new URL(window.location.href);
let currentActive = url.searchParams.get('active');
const currentTab = url.searchParams.get('tab');
const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(active?.entityUID);
const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(activeID);
if (currentTab !== String(tabIndex)) {
url.searchParams.set('tab', String(tabIndex));
}
if (active) {
if (currentActive !== String(active.entityUID)) {
url.searchParams.set('active', String(active.entityUID));
if (activeID) {
if (currentActive !== String(activeID)) {
url.searchParams.set('active', String(activeID));
}
} else {
url.searchParams.delete('active');
@ -71,7 +71,7 @@ function RSFormTabs() {
window.history.replaceState(null, '', url.toString());
}
}
}, [tabIndex, active, init]);
}, [tabIndex, activeID, init]);
return (
<div className='w-full'>

View File

@ -15,17 +15,17 @@ function TablistTools() {
const navigate = useNavigate();
const {user} = useAuth();
const { schema,
isOwned, isEditable, isTracking, readonly, forceAdmin,
isOwned, isEditable, isTracking, isReadonly: readonly, isForceAdmin: forceAdmin,
toggleTracking, toggleForceAdmin, toggleReadonly,
claim, reload, destroy, download
claim, destroy, download
} = useRSForm();
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const handleClaimOwner = useCallback(() => {
editMenu.hide();
claimOwnershipProc(claim, reload);
}, [claim, reload, editMenu]);
claimOwnershipProc(claim);
}, [claim, editMenu]);
const handleDelete = useCallback(() => {
schemaMenu.hide();
@ -69,31 +69,31 @@ function TablistTools() {
{ schemaMenu.isActive &&
<Dropdown>
<DropdownButton onClick={handleShare}>
<div className='inline-flex items-center gap-2 justify-start'>
<div className='inline-flex items-center justify-start gap-2'>
<ShareIcon color='text-primary' size={4}/>
<p>Поделиться</p>
</div>
</DropdownButton>
<DropdownButton onClick={handleClone}>
<div className='inline-flex items-center gap-2 justify-start'>
<div className='inline-flex items-center justify-start gap-2'>
<CloneIcon color='text-primary' size={4}/>
<p>Клонировать</p>
</div>
</DropdownButton>
<DropdownButton onClick={handleDownload}>
<div className='inline-flex items-center gap-2 justify-start'>
<div className='inline-flex items-center justify-start gap-2'>
<DownloadIcon color='text-primary' size={4}/>
<p>Выгрузить файл Экстеор</p>
</div>
</DropdownButton>
<DropdownButton disabled={!isEditable} onClick={handleUpload}>
<div className='inline-flex items-center gap-2 justify-start'>
<div className='inline-flex items-center justify-start gap-2'>
<UploadIcon color={isEditable ? 'text-red' : ''} size={4}/>
<p>Загрузить из Экстеора</p>
</div>
</DropdownButton>
<DropdownButton disabled={!isEditable} onClick={handleDelete}>
<span className='inline-flex items-center gap-2 justify-start'>
<span className='inline-flex items-center justify-start gap-2'>
<DumpBinIcon color={isEditable ? 'text-red' : ''} size={4} />
<p>Удалить схему</p>
</span>

View File

@ -143,7 +143,7 @@ export async function postClaimRSForm(target: string, request?: IFrontRequest) {
}
export async function postCheckExpression(schema: string, request?: IFrontRequest) {
return AxiosPost({
AxiosPost({
title: `Check expression for RSForm id=${schema}: ${request?.data['expression']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
request: request
@ -151,7 +151,7 @@ export async function postCheckExpression(schema: string, request?: IFrontReques
}
export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
return AxiosPost({
AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data['alias']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`,
request: request
@ -159,13 +159,21 @@ export async function postNewConstituenta(schema: string, request?: IFrontReques
}
export async function postDeleteConstituenta(schema: string, request?: IFrontRequest) {
return AxiosPost({
AxiosPost({
title: `Delete Constituents for RSForm id=${schema}: ${request?.data['items'].toString()}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
request: request
});
}
export async function patchMoveConstituenta(schema: string, request?: IFrontRequest) {
AxiosPatch<IRSForm>({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request?.data['items'])} to ${request?.data['move_to']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-moveto/`,
request: request
});
}
// ====== Helper functions ===========
async function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {
@ -228,13 +236,14 @@ async function AxiosDelete({endpoint, request, title}: IAxiosRequest) {
});
}
async function AxiosPatch({endpoint, request, title}: IAxiosRequest) {
async function AxiosPatch<ReturnType>({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being patrially updated`);
if (request?.setLoading) request?.setLoading(true);
axios.patch(endpoint, request?.data)
axios.patch<ReturnType>(endpoint, request?.data)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response);
return response.data;
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);

View File

@ -74,7 +74,7 @@ export enum ParsingStatus {
// Constituenta data
export interface IConstituenta {
entityUID: number
id: number
alias: string
cstType: CstType
convention?: string

View File

@ -9,16 +9,12 @@ export function shareCurrentURLProc() {
}
export async function claimOwnershipProc(
claim: (callback: BackendCallback) => Promise<void>,
reload: Function
claim: (callback: BackendCallback) => Promise<void>,
) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
}
claim(() => {
toast.success('Вы стали владельцем схемы');
reload();
});
claim(() => toast.success('Вы стали владельцем схемы'));
}
export async function deleteRSFormProc(