mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement RSForm upload and fix bugs
This commit is contained in:
parent
467f45b0c8
commit
9dd3155319
|
@ -9,4 +9,4 @@ $exclude = '*/venv/*,*/tests/*,*/migrations/*,*__init__.py,manage.py,apps.py,url
|
||||||
& $coverageExec report
|
& $coverageExec report
|
||||||
& $coverageExec html
|
& $coverageExec html
|
||||||
|
|
||||||
Start-Process "file:///D:/DEV/!WORK/Concept-Web/rsconcept/backend/htmlcov/index.html"
|
Start-Process "file:///$PSScriptRoot\backend\htmlcov\index.html"
|
|
@ -11,7 +11,7 @@ def load_initial_schemas(apps, schema_editor):
|
||||||
for subdir, dirs, files in os.walk(rootdir):
|
for subdir, dirs, files in os.walk(rootdir):
|
||||||
for file in files:
|
for file in files:
|
||||||
data = utils.read_trs(os.path.join(subdir, file))
|
data = utils.read_trs(os.path.join(subdir, file))
|
||||||
RSForm.import_json(None, data)
|
RSForm.create_from_trs(None, data)
|
||||||
|
|
||||||
|
|
||||||
def load_initial_users(apps, schema_editor):
|
def load_initial_users(apps, schema_editor):
|
||||||
|
|
|
@ -98,7 +98,7 @@ class RSForm(models.Model):
|
||||||
''' 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().only('order').aggregate(models.Max('order'))['order__max']
|
position += self.constituents().count()
|
||||||
result = Constituenta.objects.create(
|
result = Constituenta.objects.create(
|
||||||
schema=self,
|
schema=self,
|
||||||
order=position,
|
order=position,
|
||||||
|
@ -118,7 +118,7 @@ class RSForm(models.Model):
|
||||||
count_bot = 0
|
count_bot = 0
|
||||||
size = len(listCst)
|
size = len(listCst)
|
||||||
update_list = []
|
update_list = []
|
||||||
for cst in self.constituents():
|
for cst in self.constituents().only('id', 'order').order_by('order'):
|
||||||
if cst not in listCst:
|
if cst not in listCst:
|
||||||
if count_top + 1 < target:
|
if count_top + 1 < target:
|
||||||
cst.order = count_top + 1
|
cst.order = count_top + 1
|
||||||
|
@ -142,9 +142,38 @@ class RSForm(models.Model):
|
||||||
self._update_from_core()
|
self._update_from_core()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def load_trs(self, data: dict, sync_metadata: bool, skip_update: bool):
|
||||||
|
if sync_metadata:
|
||||||
|
self.title = data.get('title', 'Без названия')
|
||||||
|
self.alias = data.get('alias', '')
|
||||||
|
self.comment = data.get('comment', '')
|
||||||
|
order = 1
|
||||||
|
prev_constituents = self.constituents()
|
||||||
|
loaded_ids = set()
|
||||||
|
for cst_data in data['items']:
|
||||||
|
uid = int(cst_data['entityUID'])
|
||||||
|
if prev_constituents.filter(pk=uid).exists():
|
||||||
|
cst: Constituenta = prev_constituents.get(pk=uid)
|
||||||
|
cst.order = order
|
||||||
|
cst.load_trs(cst_data)
|
||||||
|
cst.save()
|
||||||
|
else:
|
||||||
|
cst = Constituenta.create_from_trs(cst_data, self, order)
|
||||||
|
cst.save()
|
||||||
|
uid = cst.id
|
||||||
|
loaded_ids.add(uid)
|
||||||
|
order += 1
|
||||||
|
for prev_cst in prev_constituents:
|
||||||
|
if prev_cst.id not in loaded_ids:
|
||||||
|
prev_cst.delete()
|
||||||
|
if not skip_update:
|
||||||
|
self._update_from_core()
|
||||||
|
self.save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
def create_from_trs(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
||||||
schema = RSForm.objects.create(
|
schema = RSForm.objects.create(
|
||||||
title=data.get('title', 'Без названия'),
|
title=data.get('title', 'Без названия'),
|
||||||
owner=owner,
|
owner=owner,
|
||||||
|
@ -152,15 +181,15 @@ class RSForm(models.Model):
|
||||||
comment=data.get('comment', ''),
|
comment=data.get('comment', ''),
|
||||||
is_common=is_common
|
is_common=is_common
|
||||||
)
|
)
|
||||||
schema._create_cst_from_json(data['items'])
|
schema._create_items_from_trs(data['items'])
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_trs(self) -> str:
|
||||||
''' Generate JSON string containing all data from RSForm '''
|
''' Generate JSON string containing all data from RSForm '''
|
||||||
result = self._prepare_json_rsform()
|
result = self._prepare_json_rsform()
|
||||||
items = self.constituents().order_by('order')
|
items: list['Constituenta'] = self.constituents().order_by('order')
|
||||||
for cst in items:
|
for cst in items:
|
||||||
result['items'].append(cst.to_json())
|
result['items'].append(cst.to_trs())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -178,8 +207,9 @@ class RSForm(models.Model):
|
||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def _update_from_core(self) -> dict:
|
def _update_from_core(self) -> dict:
|
||||||
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_json())))
|
checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs())))
|
||||||
update_list = self.constituents().only('id', 'order')
|
update_list = self.constituents().only('id', 'order')
|
||||||
if (len(checked['items']) != update_list.count()):
|
if (len(checked['items']) != update_list.count()):
|
||||||
raise ValidationError
|
raise ValidationError
|
||||||
|
@ -194,10 +224,12 @@ class RSForm(models.Model):
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||||
return checked
|
return checked
|
||||||
|
|
||||||
def _create_cst_from_json(self, items):
|
@transaction.atomic
|
||||||
|
def _create_items_from_trs(self, items):
|
||||||
order = 1
|
order = 1
|
||||||
for cst in items:
|
for cst in items:
|
||||||
Constituenta.import_json(cst, self, order)
|
object = Constituenta.create_from_trs(cst, self, order)
|
||||||
|
object.save()
|
||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -270,28 +302,43 @@ class Constituenta(models.Model):
|
||||||
return self.alias
|
return self.alias
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def import_json(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
def create_from_trs(data: dict, schema: RSForm, order: int) -> 'Constituenta':
|
||||||
|
''' Create constituenta from TRS json '''
|
||||||
cst = Constituenta(
|
cst = Constituenta(
|
||||||
alias=data['alias'],
|
alias=data['alias'],
|
||||||
schema=schema,
|
schema=schema,
|
||||||
order=order,
|
order=order,
|
||||||
cst_type=data['cstType'],
|
cst_type=data['cstType'],
|
||||||
convention=data.get('convention', 'Без названия')
|
|
||||||
)
|
)
|
||||||
if 'definition' in data:
|
cst._load_texts(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['term'].get('raw', '')
|
|
||||||
cst.term_resolved = data['term'].get('resolved', '')
|
|
||||||
cst.term_forms = data['term'].get('forms', [])
|
|
||||||
cst.save()
|
|
||||||
return cst
|
return cst
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def load_trs(self, data: dict):
|
||||||
|
''' Load data from TRS json '''
|
||||||
|
self.alias = data['alias']
|
||||||
|
self.cst_type = data['cstType']
|
||||||
|
self._load_texts(data)
|
||||||
|
|
||||||
|
def _load_texts(self, data: dict):
|
||||||
|
self.convention = data.get('convention', '')
|
||||||
|
if 'definition' in data:
|
||||||
|
self.definition_formal = data['definition'].get('formal', '')
|
||||||
|
if 'text' in data['definition']:
|
||||||
|
self.definition_raw = data['definition']['text'].get('raw', '')
|
||||||
|
self.definition_resolved = data['definition']['text'].get('resolved', '')
|
||||||
|
else:
|
||||||
|
self.definition_raw = ''
|
||||||
|
self.definition_resolved = ''
|
||||||
|
if 'term' in data:
|
||||||
|
self.term_raw = data['term'].get('raw', '')
|
||||||
|
self.term_resolved = data['term'].get('resolved', '')
|
||||||
|
self.term_forms = data['term'].get('forms', [])
|
||||||
|
else:
|
||||||
|
self.term_raw = ''
|
||||||
|
self.term_resolved = ''
|
||||||
|
self.term_forms = []
|
||||||
|
|
||||||
|
def to_trs(self) -> str:
|
||||||
return {
|
return {
|
||||||
'entityUID': self.id,
|
'entityUID': self.id,
|
||||||
'type': 'constituenta',
|
'type': 'constituenta',
|
||||||
|
|
|
@ -20,6 +20,23 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ('owner', 'id')
|
read_only_fields = ('owner', 'id')
|
||||||
|
|
||||||
|
|
||||||
|
class RSFormUploadSerializer(serializers.Serializer):
|
||||||
|
file = serializers.FileField()
|
||||||
|
load_metadata = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
|
class RSFormContentsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RSForm
|
||||||
|
|
||||||
|
def to_representation(self, instance: RSForm):
|
||||||
|
result = RSFormSerializer(instance).data
|
||||||
|
result['items'] = []
|
||||||
|
for cst in instance.constituents().order_by('order'):
|
||||||
|
result['items'].append(ConstituentaSerializer(cst).data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ConstituentaSerializer(serializers.ModelSerializer):
|
class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Constituenta
|
model = Constituenta
|
||||||
|
@ -79,7 +96,7 @@ class RSFormDetailsSerlializer(serializers.BaseSerializer):
|
||||||
model = RSForm
|
model = RSForm
|
||||||
|
|
||||||
def to_representation(self, instance: RSForm):
|
def to_representation(self, instance: RSForm):
|
||||||
trs = pyconcept.check_schema(json.dumps(instance.to_json()))
|
trs = pyconcept.check_schema(json.dumps(instance.to_trs()))
|
||||||
trs = trs.replace('entityUID', 'id')
|
trs = trs.replace('entityUID', 'id')
|
||||||
result = json.loads(trs)
|
result = json.loads(trs)
|
||||||
result['id'] = instance.id
|
result['id'] = instance.id
|
||||||
|
|
|
@ -209,7 +209,7 @@ class TestRSForm(TestCase):
|
||||||
self.assertEqual(x1.order, 2)
|
self.assertEqual(x1.order, 2)
|
||||||
self.assertEqual(x2.order, 1)
|
self.assertEqual(x2.order, 1)
|
||||||
|
|
||||||
def test_to_json(self):
|
def test_to_trs(self):
|
||||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
||||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
||||||
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
x2 = schema.insert_at(1, 'X2', CstType.BASE)
|
||||||
|
@ -223,9 +223,9 @@ class TestRSForm(TestCase):
|
||||||
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
f'"term": {{"raw": "", "resolved": "", "forms": []}}, '
|
||||||
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
f'"definition": {{"formal": "", "text": {{"raw": "", "resolved": ""}}}}}}]}}'
|
||||||
)
|
)
|
||||||
self.assertEqual(schema.to_json(), expected)
|
self.assertEqual(schema.to_trs(), expected)
|
||||||
|
|
||||||
def test_import_json(self):
|
def test_create_from_trs(self):
|
||||||
input = json.loads(
|
input = json.loads(
|
||||||
'{"type": "rsform", "title": "Test", "alias": "KS1", '
|
'{"type": "rsform", "title": "Test", "alias": "KS1", '
|
||||||
'"comment": "Test", "items": '
|
'"comment": "Test", "items": '
|
||||||
|
@ -236,7 +236,7 @@ class TestRSForm(TestCase):
|
||||||
'"term": {"raw": "", "resolved": ""}, '
|
'"term": {"raw": "", "resolved": ""}, '
|
||||||
'"definition": {"formal": "", "text": {"raw": "", "resolved": ""}}}]}'
|
'"definition": {"formal": "", "text": {"raw": "", "resolved": ""}}}]}'
|
||||||
)
|
)
|
||||||
schema = RSForm.import_json(self.user1, input, False)
|
schema = RSForm.create_from_trs(self.user1, input, False)
|
||||||
self.assertEqual(schema.owner, self.user1)
|
self.assertEqual(schema.owner, self.user1)
|
||||||
self.assertEqual(schema.title, 'Test')
|
self.assertEqual(schema.title, 'Test')
|
||||||
self.assertEqual(schema.alias, 'KS1')
|
self.assertEqual(schema.alias, 'KS1')
|
||||||
|
@ -245,3 +245,28 @@ class TestRSForm(TestCase):
|
||||||
self.assertEqual(constituents.count(), 2)
|
self.assertEqual(constituents.count(), 2)
|
||||||
self.assertEqual(constituents[0].alias, 'X1')
|
self.assertEqual(constituents[0].alias, 'X1')
|
||||||
self.assertEqual(constituents[0].definition_formal, '123')
|
self.assertEqual(constituents[0].definition_formal, '123')
|
||||||
|
|
||||||
|
def test_load_trs(self):
|
||||||
|
schema = RSForm.objects.create(title='Test', owner=self.user1, alias='КС1')
|
||||||
|
x2 = schema.insert_last('X2', CstType.BASE)
|
||||||
|
schema.insert_last('X3', CstType.BASE)
|
||||||
|
input = json.loads(
|
||||||
|
'{"title": "Test1", "alias": "KS1", '
|
||||||
|
'"comment": "Test", "items": '
|
||||||
|
'[{"entityUID": "' + str(x2.id) + '", "cstType": "basic", "alias": "X1", "convention": "test", '
|
||||||
|
'"term": {"raw": "t1", "resolved": "t2"}, '
|
||||||
|
'"definition": {"formal": "123", "text": {"raw": "t3", "resolved": "t4"}}}]}'
|
||||||
|
)
|
||||||
|
schema.load_trs(input, sync_metadata=True, skip_update=True)
|
||||||
|
x2.refresh_from_db()
|
||||||
|
self.assertEqual(schema.constituents().count(), 1)
|
||||||
|
self.assertEqual(schema.title, input['title'])
|
||||||
|
self.assertEqual(schema.alias, input['alias'])
|
||||||
|
self.assertEqual(schema.comment, input['comment'])
|
||||||
|
self.assertEqual(x2.alias, input['items'][0]['alias'])
|
||||||
|
self.assertEqual(x2.convention, input['items'][0]['convention'])
|
||||||
|
self.assertEqual(x2.term_raw, input['items'][0]['term']['raw'])
|
||||||
|
self.assertEqual(x2.term_resolved, input['items'][0]['term']['resolved'])
|
||||||
|
self.assertEqual(x2.definition_formal, input['items'][0]['definition']['formal'])
|
||||||
|
self.assertEqual(x2.definition_raw, input['items'][0]['definition']['text']['raw'])
|
||||||
|
self.assertEqual(x2.definition_resolved, input['items'][0]['definition']['text']['resolved'])
|
||||||
|
|
|
@ -115,7 +115,6 @@ class TestRSFormViewset(APITestCase):
|
||||||
schema.insert_last(alias='X1', type=CstType.BASE)
|
schema.insert_last(alias='X1', type=CstType.BASE)
|
||||||
response = self.client.get(f'/api/rsforms/{schema.id}/contents/')
|
response = self.client.get(f'/api/rsforms/{schema.id}/contents/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.data, schema.to_json())
|
|
||||||
|
|
||||||
def test_details(self):
|
def test_details(self):
|
||||||
schema = RSForm.objects.create(title='Test')
|
schema = RSForm.objects.create(title='Test')
|
||||||
|
@ -248,6 +247,57 @@ class TestRSFormViewset(APITestCase):
|
||||||
data=data, content_type='application/json')
|
data=data, content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_reset_aliases(self):
|
||||||
|
schema = self.rsform_owned
|
||||||
|
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data['id'], schema.id)
|
||||||
|
|
||||||
|
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=1)
|
||||||
|
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=2)
|
||||||
|
d11 = Constituenta.objects.create(schema=schema, alias='D11', cst_type='term', order=3)
|
||||||
|
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||||
|
x1.refresh_from_db()
|
||||||
|
x2.refresh_from_db()
|
||||||
|
d11.refresh_from_db()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(x2.order, 1)
|
||||||
|
self.assertEqual(x2.alias, 'X1')
|
||||||
|
self.assertEqual(x1.order, 2)
|
||||||
|
self.assertEqual(x1.alias, 'X2')
|
||||||
|
self.assertEqual(d11.order, 3)
|
||||||
|
self.assertEqual(d11.alias, 'D1')
|
||||||
|
|
||||||
|
response = self.client.patch(f'/api/rsforms/{schema.id}/reset-aliases/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_load_trs(self):
|
||||||
|
schema = self.rsform_owned
|
||||||
|
schema.title = 'Testt11'
|
||||||
|
schema.save()
|
||||||
|
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
|
||||||
|
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||||
|
data = {'file': file, 'load_metadata': False}
|
||||||
|
response = self.client.patch(f'/api/rsforms/{schema.id}/load-trs/', data=data, format='multipart')
|
||||||
|
schema.refresh_from_db()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(schema.title, 'Testt11')
|
||||||
|
self.assertEqual(len(response.data['items']), 25)
|
||||||
|
self.assertEqual(schema.constituents().count(), 25)
|
||||||
|
self.assertFalse(Constituenta.objects.all().filter(pk=x1.id).exists())
|
||||||
|
|
||||||
|
def test_clone(self):
|
||||||
|
schema = self.rsform_owned
|
||||||
|
schema.title = 'Testt11'
|
||||||
|
schema.save()
|
||||||
|
x1 = Constituenta.objects.create(schema=schema, alias='X12', cst_type='basic', order=1)
|
||||||
|
data = json.dumps({'title': 'Title'})
|
||||||
|
response = self.client.post(f'/api/rsforms/{schema.id}/clone/', data=data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(response.data['title'], 'Title')
|
||||||
|
self.assertEqual(response.data['items'][0]['alias'], x1.alias)
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionalViews(APITestCase):
|
class TestFunctionalViews(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -34,6 +34,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
ordering_fields = ('owner', 'title', 'time_update')
|
ordering_fields = ('owner', 'title', 'time_update')
|
||||||
ordering = ('-time_update')
|
ordering = ('-time_update')
|
||||||
|
|
||||||
|
def _get_schema(self) -> models.RSForm:
|
||||||
|
return self.get_object()
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
|
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
|
||||||
return serializer.save(owner=self.request.user)
|
return serializer.save(owner=self.request.user)
|
||||||
|
@ -41,10 +44,10 @@ 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', 'load_trs',
|
||||||
'cst_create', 'cst_multidelete']:
|
'cst_create', 'cst_multidelete', 'reset_aliases']:
|
||||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
permission_classes = [utils.ObjectOwnerOrAdmin]
|
||||||
elif self.action in ['create', 'claim']:
|
elif self.action in ['create', 'claim', 'clone']:
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
else:
|
else:
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
@ -53,7 +56,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['post'], url_path='cst-create')
|
@action(detail=True, methods=['post'], url_path='cst-create')
|
||||||
def cst_create(self, request, pk):
|
def cst_create(self, request, pk):
|
||||||
''' Create new constituenta '''
|
''' Create new constituenta '''
|
||||||
schema: models.RSForm = self.get_object()
|
schema = self._get_schema()
|
||||||
serializer = serializers.CstCreateSerializer(data=request.data)
|
serializer = serializers.CstCreateSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
if ('insert_after' in serializer.validated_data and serializer.validated_data['insert_after'] is not None):
|
if ('insert_after' in serializer.validated_data and serializer.validated_data['insert_after'] is not None):
|
||||||
|
@ -74,7 +77,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
@action(detail=True, methods=['patch'], url_path='cst-multidelete')
|
||||||
def cst_multidelete(self, request, pk):
|
def cst_multidelete(self, request, pk):
|
||||||
''' Delete multiple constituents '''
|
''' Delete multiple constituents '''
|
||||||
schema: models.RSForm = self.get_object()
|
schema = self._get_schema()
|
||||||
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.delete_cst(serializer.validated_data['constituents'])
|
schema.delete_cst(serializer.validated_data['constituents'])
|
||||||
|
@ -85,7 +88,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
@action(detail=True, methods=['patch'], url_path='cst-moveto')
|
||||||
def cst_moveto(self, request, pk):
|
def cst_moveto(self, request, pk):
|
||||||
''' Delete multiple constituents '''
|
''' Delete multiple constituents '''
|
||||||
schema: models.RSForm = self.get_object()
|
schema: models.RSForm = self._get_schema()
|
||||||
serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema})
|
serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to'])
|
||||||
|
@ -93,9 +96,46 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||||
return Response(status=200, data=outSerializer.data)
|
return Response(status=200, data=outSerializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||||
|
def reset_aliases(self, request, pk):
|
||||||
|
''' Recreate all aliases based on order '''
|
||||||
|
schema = self._get_schema()
|
||||||
|
result = json.loads(pyconcept.reset_aliases(json.dumps(schema.to_trs())))
|
||||||
|
schema.load_trs(data=result, sync_metadata=False, skip_update=True)
|
||||||
|
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||||
|
return Response(status=200, data=outSerializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['patch'], url_path='load-trs')
|
||||||
|
def load_trs(self, request, pk):
|
||||||
|
''' Load data from file and replace current schema '''
|
||||||
|
serializer = serializers.RSFormUploadSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
schema = self._get_schema()
|
||||||
|
load_metadata = serializer.validated_data['load_metadata']
|
||||||
|
data = utils.read_trs(request.FILES['file'].file)
|
||||||
|
schema.load_trs(data, load_metadata, skip_update=False)
|
||||||
|
outSerializer = serializers.RSFormDetailsSerlializer(schema)
|
||||||
|
return Response(status=200, data=outSerializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='clone')
|
||||||
|
def clone(self, request, pk):
|
||||||
|
''' Clone RSForm constituents and create new schema using new metadata '''
|
||||||
|
serializer = serializers.RSFormSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
new_schema = models.RSForm.objects.create(
|
||||||
|
title=serializer.validated_data['title'],
|
||||||
|
owner=self.request.user,
|
||||||
|
alias=serializer.validated_data.get('alias', ''),
|
||||||
|
comment=serializer.validated_data.get('comment', ''),
|
||||||
|
is_common=serializer.validated_data.get('is_common', False),
|
||||||
|
)
|
||||||
|
new_schema.load_trs(data=self._get_schema().to_trs(), sync_metadata=False, skip_update=True)
|
||||||
|
outSerializer = serializers.RSFormDetailsSerlializer(new_schema)
|
||||||
|
return Response(status=201, data=outSerializer.data)
|
||||||
|
|
||||||
@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 = self._get_schema()
|
||||||
if schema.owner == self.request.user:
|
if schema.owner == self.request.user:
|
||||||
return Response(status=304)
|
return Response(status=304)
|
||||||
else:
|
else:
|
||||||
|
@ -105,21 +145,21 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def contents(self, request, pk):
|
def contents(self, request, pk):
|
||||||
''' View schema contents (including constituents) '''
|
''' View schema db contents (including constituents) '''
|
||||||
schema = self.get_object().to_json()
|
schema = serializers.RSFormContentsSerializer(self._get_schema()).data
|
||||||
return Response(schema)
|
return Response(schema)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def details(self, request, pk):
|
def details(self, request, pk):
|
||||||
''' Detailed schema view including statuses '''
|
''' Detailed schema view including statuses '''
|
||||||
schema: models.RSForm = self.get_object()
|
schema = self._get_schema()
|
||||||
serializer = serializers.RSFormDetailsSerlializer(schema)
|
serializer = serializers.RSFormDetailsSerlializer(schema)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def check(self, request, pk):
|
def check(self, request, pk):
|
||||||
''' Check RS expression against schema context '''
|
''' Check RS expression against schema context '''
|
||||||
schema = self.get_object().to_json()
|
schema = self._get_schema().to_trs()
|
||||||
serializer = serializers.ExpressionSerializer(data=request.data)
|
serializer = serializers.ExpressionSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
expression = serializer.validated_data['expression']
|
expression = serializer.validated_data['expression']
|
||||||
|
@ -129,9 +169,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||||
def export_trs(self, request, pk):
|
def export_trs(self, request, pk):
|
||||||
''' Download Exteor compatible file '''
|
''' Download Exteor compatible file '''
|
||||||
schema = self.get_object().to_json()
|
schema = self._get_schema().to_trs()
|
||||||
trs = utils.write_trs(schema)
|
trs = utils.write_trs(schema)
|
||||||
filename = self.get_object().alias
|
filename = self._get_schema().alias
|
||||||
if filename == '' or not filename.isascii():
|
if filename == '' or not filename.isascii():
|
||||||
# Note: non-ascii symbols in Content-Disposition
|
# Note: non-ascii symbols in Content-Disposition
|
||||||
# are not supported by some browsers
|
# are not supported by some browsers
|
||||||
|
@ -152,7 +192,7 @@ class TrsImportView(views.APIView):
|
||||||
owner = self.request.user
|
owner = self.request.user
|
||||||
if owner.is_anonymous:
|
if owner.is_anonymous:
|
||||||
owner = None
|
owner = None
|
||||||
schema = models.RSForm.import_json(owner, data)
|
schema = models.RSForm.create_from_trs(owner, data)
|
||||||
result = serializers.RSFormSerializer(schema)
|
result = serializers.RSFormSerializer(schema)
|
||||||
return Response(status=201, data=result.data)
|
return Response(status=201, data=result.data)
|
||||||
|
|
||||||
|
@ -167,11 +207,11 @@ def create_rsform(request):
|
||||||
serializer = serializers.RSFormSerializer(data=request.data)
|
serializer = serializers.RSFormSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema = models.RSForm.objects.create(
|
schema = models.RSForm.objects.create(
|
||||||
title=request.data['title'],
|
title=serializer.validated_data['title'],
|
||||||
owner=owner,
|
owner=owner,
|
||||||
alias=request.data.get('alias', ''),
|
alias=serializer.validated_data.get('alias', ''),
|
||||||
comment=request.data.get('comment', ''),
|
comment=serializer.validated_data.get('comment', ''),
|
||||||
is_common=request.data.get('is_common', False),
|
is_common=serializer.validated_data.get('is_common', False),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = utils.read_trs(request.FILES['file'].file)
|
data = utils.read_trs(request.FILES['file'].file)
|
||||||
|
@ -186,7 +226,7 @@ def create_rsform(request):
|
||||||
is_common = True
|
is_common = True
|
||||||
if ('is_common' in request.data):
|
if ('is_common' in request.data):
|
||||||
is_common = request.data['is_common'] == 'true'
|
is_common = request.data['is_common'] == 'true'
|
||||||
schema = models.RSForm.import_json(owner, data, is_common)
|
schema = models.RSForm.create_from_trs(owner, data, is_common)
|
||||||
result = serializers.RSFormSerializer(schema)
|
result = serializers.RSFormSerializer(schema)
|
||||||
return Response(status=201, data=result.data)
|
return Response(status=201, data=result.data)
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ class LoginSerializer(serializers.Serializer):
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
username = attrs.get('username')
|
username = attrs.get('username')
|
||||||
password = attrs.get('password')
|
password = attrs.get('password')
|
||||||
if username and password:
|
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
request=self.context.get('request'),
|
request=self.context.get('request'),
|
||||||
username=username,
|
username=username,
|
||||||
|
@ -30,9 +29,6 @@ class LoginSerializer(serializers.Serializer):
|
||||||
if not user:
|
if not user:
|
||||||
msg = 'Неправильное сочетание имени пользователя и пароля.'
|
msg = 'Неправильное сочетание имени пользователя и пароля.'
|
||||||
raise serializers.ValidationError(msg, code='authorization')
|
raise serializers.ValidationError(msg, code='authorization')
|
||||||
else:
|
|
||||||
msg = 'Заполните оба поля: Имя пользователя и Пароль.'
|
|
||||||
raise serializers.ValidationError(msg, code='authorization')
|
|
||||||
attrs['user'] = user
|
attrs['user'] = user
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
|
@ -29,3 +29,9 @@ class TestLoginSerializer(APITestCase):
|
||||||
request = self.factory.post('/users/api/login', data)
|
request = self.factory.post('/users/api/login', data)
|
||||||
serializer = LoginSerializer(data=data, context={'request': request})
|
serializer = LoginSerializer(data=data, context={'request': request})
|
||||||
self.assertFalse(serializer.is_valid(raise_exception=False))
|
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||||
|
|
||||||
|
def test_validate_empty_username(self):
|
||||||
|
data = {'username': '', 'auth': 'invalid'}
|
||||||
|
request = self.factory.post('/users/api/login', data)
|
||||||
|
serializer = LoginSerializer(data=data, context={'request': request})
|
||||||
|
self.assertFalse(serializer.is_valid(raise_exception=False))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
interface ButtonProps
|
interface ButtonProps
|
||||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> {
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> {
|
||||||
text?: string
|
text?: string
|
||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Button from './Button';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface FileInputProps {
|
interface FileInputProps {
|
||||||
id: string
|
id?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
label: string
|
label: string
|
||||||
acceptType?: string
|
acceptType?: string
|
||||||
|
@ -33,7 +33,7 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex gap-2 py-2 mt-3 items-center ' + widthClass}>
|
<div className={'flex flex-col gap-2 py-2 [&:not(:first-child)]:mt-3 items-start ' + widthClass}>
|
||||||
<input id={id} type='file'
|
<input id={id} type='file'
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
required={required}
|
required={required}
|
||||||
|
|
20
rsconcept/frontend/src/components/Common/MiniButton.tsx
Normal file
20
rsconcept/frontend/src/components/Common/MiniButton.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
interface MiniButtonProps
|
||||||
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'title' > {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniButton({ icon, tooltip, children, ...props }: MiniButtonProps) {
|
||||||
|
return (
|
||||||
|
<button type='button'
|
||||||
|
title={tooltip}
|
||||||
|
className='px-1 py-1 font-bold rounded-full cursor-pointer whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon && <span>{icon}</span>}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MiniButton;
|
|
@ -39,7 +39,7 @@ function Modal({ title, show, hideWindow, onSubmit, onCancel, canSubmit, childre
|
||||||
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
||||||
</div>
|
</div>
|
||||||
<div ref={ref} className='fixed bottom-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit z-[60] clr-card border shadow-md'>
|
<div ref={ref} className='fixed bottom-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit z-[60] clr-card border shadow-md'>
|
||||||
{ title && <h1 className='mb-4 text-xl font-bold'>{title}</h1> }
|
{ title && <h1 className='mb-4 text-xl font-bold text-center'>{title}</h1> }
|
||||||
<div className='py-2'>
|
<div className='py-2'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,12 +5,12 @@ import { type ErrorInfo } from '../components/BackendError'
|
||||||
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
||||||
import {
|
import {
|
||||||
type DataCallback, deleteRSForm, getTRSFile,
|
type DataCallback, deleteRSForm, getTRSFile,
|
||||||
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchRSForm,
|
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchResetAliases,
|
||||||
postClaimRSForm, postNewConstituenta
|
patchRSForm,
|
||||||
} from '../utils/backendAPI'
|
patchUploadTRS, postClaimRSForm, postNewConstituenta} from '../utils/backendAPI'
|
||||||
import {
|
import {
|
||||||
IConstituenta, IConstituentaList, IConstituentaMeta, ICstCreateData,
|
IConstituenta, IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormMeta, IRSFormUpdateData
|
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||||
} from '../utils/models'
|
} from '../utils/models'
|
||||||
import { useAuth } from './AuthContext'
|
import { useAuth } from './AuthContext'
|
||||||
|
|
||||||
|
@ -36,9 +36,11 @@ interface IRSFormContext {
|
||||||
toggleTracking: () => void
|
toggleTracking: () => void
|
||||||
|
|
||||||
update: (data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => void
|
update: (data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => void
|
||||||
destroy: (callback?: DataCallback) => void
|
destroy: (callback?: () => void) => void
|
||||||
claim: (callback?: DataCallback<IRSFormMeta>) => void
|
claim: (callback?: DataCallback<IRSFormMeta>) => void
|
||||||
download: (callback: DataCallback<Blob>) => void
|
download: (callback: DataCallback<Blob>) => void
|
||||||
|
upload: (data: IRSFormUploadData, callback: () => void) => void
|
||||||
|
resetAliases: (callback: () => void) => void
|
||||||
|
|
||||||
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void
|
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void
|
||||||
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void
|
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void
|
||||||
|
@ -114,16 +116,34 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, setSchema, schema])
|
}, [schemaID, setError, setSchema, schema])
|
||||||
|
|
||||||
|
const upload = useCallback(
|
||||||
|
(data: IRSFormUploadData, callback?: () => void) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
patchUploadTRS(schemaID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => { setError(error) },
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(newData);
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [schemaID, setError, setSchema, schema])
|
||||||
|
|
||||||
const destroy = useCallback(
|
const destroy = useCallback(
|
||||||
(callback?: DataCallback) => {
|
(callback?: () => void) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
deleteRSForm(schemaID, {
|
deleteRSForm(schemaID, {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => { setError(error) },
|
onError: error => { setError(error) },
|
||||||
onSuccess: newData => {
|
onSuccess: () => {
|
||||||
setSchema(undefined);
|
setSchema(undefined);
|
||||||
if (callback) callback(newData);
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, setSchema])
|
}, [schemaID, setError, setSchema])
|
||||||
|
@ -145,6 +165,23 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, schema, user, setSchema])
|
}, [schemaID, setError, schema, user, setSchema])
|
||||||
|
|
||||||
|
const resetAliases = useCallback(
|
||||||
|
(callback?: () => void) => {
|
||||||
|
if (!schema || !user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
patchResetAliases(schemaID, {
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => { setError(error) },
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(Object.assign(schema, newData));
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [schemaID, setError, schema, user, setSchema])
|
||||||
|
|
||||||
const download = useCallback(
|
const download = useCallback(
|
||||||
(callback: DataCallback<Blob>) => {
|
(callback: DataCallback<Blob>) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
|
@ -218,29 +255,15 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
return (
|
return (
|
||||||
<RSFormContext.Provider value={{
|
<RSFormContext.Provider value={{
|
||||||
schema,
|
schema,
|
||||||
error,
|
error, loading, processing,
|
||||||
loading,
|
activeID, activeCst, setActiveID,
|
||||||
processing,
|
isForceAdmin, isReadonly, isOwned, isEditable,
|
||||||
activeID,
|
isClaimable, isTracking,
|
||||||
activeCst,
|
|
||||||
setActiveID,
|
|
||||||
isForceAdmin,
|
|
||||||
isReadonly,
|
|
||||||
toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
|
toggleForceAdmin: () => { setIsForceAdmin(prev => !prev) },
|
||||||
toggleReadonly: () => { setIsReadonly(prev => !prev) },
|
toggleReadonly: () => { setIsReadonly(prev => !prev) },
|
||||||
isOwned,
|
|
||||||
isEditable,
|
|
||||||
isClaimable,
|
|
||||||
isTracking,
|
|
||||||
toggleTracking,
|
toggleTracking,
|
||||||
update,
|
update, download, upload, destroy, claim, resetAliases,
|
||||||
download,
|
cstUpdate, cstCreate, cstDelete, cstMoveTo
|
||||||
destroy,
|
|
||||||
claim,
|
|
||||||
cstUpdate,
|
|
||||||
cstCreate,
|
|
||||||
cstDelete,
|
|
||||||
cstMoveTo
|
|
||||||
}}>
|
}}>
|
||||||
{ children }
|
{ children }
|
||||||
</RSFormContext.Provider>
|
</RSFormContext.Provider>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
|
|
||||||
/* Transparent button */
|
/* Transparent button */
|
||||||
.clr-btn-clear {
|
.clr-btn-clear {
|
||||||
@apply hover:bg-gray-300 dark:hover:bg-gray-400
|
@apply hover:bg-gray-300 dark:hover:bg-gray-400 dark:disabled:text-zinc-400 disabled:text-gray-400 text-gray-500 dark:text-zinc-200
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-checkbox {
|
.clr-checkbox {
|
||||||
|
|
|
@ -53,7 +53,7 @@ function RSFormCreatePage() {
|
||||||
file: file,
|
file: file,
|
||||||
fileName: file?.name
|
fileName: file?.name
|
||||||
};
|
};
|
||||||
void createSchema(data, onSuccess);
|
createSchema(data, onSuccess);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
import SubmitButton from '../../components/Common/SubmitButton';
|
import SubmitButton from '../../components/Common/SubmitButton';
|
||||||
import TextArea from '../../components/Common/TextArea';
|
import TextArea from '../../components/Common/TextArea';
|
||||||
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||||
|
@ -11,7 +12,7 @@ import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
|
||||||
import ConstituentsSideList from './ConstituentsSideList';
|
import ConstituentsSideList from './ConstituentsSideList';
|
||||||
import CreateCstModal from './CreateCstModal';
|
import CreateCstModal from './CreateCstModal';
|
||||||
import ExpressionEditor from './ExpressionEditor';
|
import ExpressionEditor from './ExpressionEditor';
|
||||||
import { RSFormTabsList } from './RSFormTabs';
|
import { RSTabsList } from './RSTabs';
|
||||||
|
|
||||||
function ConstituentEditor() {
|
function ConstituentEditor() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -113,7 +114,7 @@ function ConstituentEditor() {
|
||||||
insert_after: activeID
|
insert_after: activeID
|
||||||
}
|
}
|
||||||
cstCreate(data, newCst => {
|
cstCreate(data, newCst => {
|
||||||
navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${newCst.id}`);
|
navigate(`/rsforms/${schema.id}?tab=${RSTabsList.CST_EDIT}&active=${newCst.id}`);
|
||||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -167,22 +168,18 @@ function ConstituentEditor() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-end'>
|
<div className='flex justify-end'>
|
||||||
<button type='button'
|
<MiniButton
|
||||||
title='Создать конституенты после данной'
|
tooltip='Создать конституенты после данной'
|
||||||
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
onClick={() => { handleAddNew(); }}
|
onClick={() => { handleAddNew(); }}
|
||||||
>
|
icon={<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />}
|
||||||
<SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />
|
/>
|
||||||
</button>
|
<MiniButton
|
||||||
<button type='button'
|
tooltip='Удалить редактируемую конституенту'
|
||||||
title='Удалить редактируемую конституенту'
|
|
||||||
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
|
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
>
|
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />}
|
||||||
<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TextArea id='term' label='Термин'
|
<TextArea id='term' label='Термин'
|
||||||
|
|
|
@ -18,13 +18,14 @@ interface ConstituentsTableProps {
|
||||||
function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
const {
|
const {
|
||||||
schema, isEditable,
|
schema, isEditable,
|
||||||
cstCreate, cstDelete, cstMoveTo
|
cstCreate, cstDelete, cstMoveTo, resetAliases
|
||||||
} = useRSForm();
|
} = useRSForm();
|
||||||
const { noNavigation } = useConceptTheme();
|
const { noNavigation } = useConceptTheme();
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
const [showCstModal, setShowCstModal] = useState(false);
|
const [showCstModal, setShowCstModal] = useState(false);
|
||||||
|
const [toggledClearRows, setToggledClearRows] = useState(false);
|
||||||
|
|
||||||
const handleRowClicked = useCallback(
|
const handleRowClicked = useCallback(
|
||||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
|
@ -39,7 +40,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
selectedCount: number
|
selectedCount: number
|
||||||
selectedRows: IConstituenta[]
|
selectedRows: IConstituenta[]
|
||||||
}) => {
|
}) => {
|
||||||
setSelected(selectedRows.map((cst) => cst.id));
|
setSelected(selectedRows.map(cst => cst.id));
|
||||||
}, [setSelected]);
|
}, [setSelected]);
|
||||||
|
|
||||||
// Delete selected constituents
|
// Delete selected constituents
|
||||||
|
@ -50,8 +51,11 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
const data = {
|
const data = {
|
||||||
items: selected.map(id => { return { id }; })
|
items: selected.map(id => { return { id }; })
|
||||||
}
|
}
|
||||||
const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias).join(', ');
|
const deletedNames = selected.map(id => schema.items?.find(cst => cst.id === id)?.alias).join(', ');
|
||||||
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames}`));
|
cstDelete(data, () => {
|
||||||
|
toast.success(`Конституенты удалены: ${deletedNames}`);
|
||||||
|
setToggledClearRows(prev => !prev);
|
||||||
|
});
|
||||||
}, [selected, schema?.items, cstDelete]);
|
}, [selected, schema?.items, cstDelete]);
|
||||||
|
|
||||||
// Move selected cst up
|
// Move selected cst up
|
||||||
|
@ -70,7 +74,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
}, -1);
|
}, -1);
|
||||||
const target = Math.max(0, currentIndex - 1) + 1
|
const target = Math.max(0, currentIndex - 1) + 1
|
||||||
const data = {
|
const data = {
|
||||||
items: selected.map(id => { return { id }; }),
|
items: selected.map(id => { return { id: id }; }),
|
||||||
move_to: target
|
move_to: target
|
||||||
}
|
}
|
||||||
cstMoveTo(data);
|
cstMoveTo(data);
|
||||||
|
@ -96,7 +100,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
}, -1);
|
}, -1);
|
||||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
|
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
|
||||||
const data: ICstMovetoData = {
|
const data: ICstMovetoData = {
|
||||||
items: selected.map(id => { return { id }; }),
|
items: selected.map(id => { return { id: id }; }),
|
||||||
move_to: target
|
move_to: target
|
||||||
}
|
}
|
||||||
cstMoveTo(data);
|
cstMoveTo(data);
|
||||||
|
@ -104,8 +108,8 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
|
|
||||||
// Generate new names for all constituents
|
// Generate new names for all constituents
|
||||||
const handleReindex = useCallback(() => {
|
const handleReindex = useCallback(() => {
|
||||||
toast.info('Переиндексация');
|
resetAliases(() => toast.success('Переиндексация конституент успешна'));
|
||||||
}, []);
|
}, [resetAliases]);
|
||||||
|
|
||||||
// Add new constituent
|
// Add new constituent
|
||||||
const handleAddNew = useCallback((type?: CstType) => {
|
const handleAddNew = useCallback((type?: CstType) => {
|
||||||
|
@ -130,7 +134,15 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
|
|
||||||
// Implement hotkeys for working with constituents table
|
// Implement hotkeys for working with constituents table
|
||||||
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (!event.altKey || !isEditable || event.shiftKey) {
|
if (!isEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'Delete' && selected.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleDelete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.altKey || event.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (processAltKey(event.key)) {
|
if (processAltKey(event.key)) {
|
||||||
|
@ -335,7 +347,11 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
})}
|
})}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}>
|
<div className='w-full h-full'
|
||||||
|
onKeyDown={handleTableKey}
|
||||||
|
tabIndex={0}
|
||||||
|
title='Горячие клавиши:
Двойной клик / Alt + клик - редактирование конституенты
Alt + вверх/вниз - движение конституент
Delete - удаление выбранных
Alt + 1-6, Q,W - добавление конституент'
|
||||||
|
>
|
||||||
<DataTableThemed
|
<DataTableThemed
|
||||||
data={schema?.items ?? []}
|
data={schema?.items ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
@ -356,6 +372,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
||||||
onSelectedRowsChange={handleSelectionChange}
|
onSelectedRowsChange={handleSelectionChange}
|
||||||
onRowDoubleClicked={onOpenEdit}
|
onRowDoubleClicked={onOpenEdit}
|
||||||
onRowClicked={handleRowClicked}
|
onRowClicked={handleRowClicked}
|
||||||
|
clearSelectedRows={toggledClearRows}
|
||||||
dense
|
dense
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { useIntl } from 'react-intl';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import Button from '../../components/Common/Button';
|
|
||||||
import Checkbox from '../../components/Common/Checkbox';
|
import Checkbox from '../../components/Common/Checkbox';
|
||||||
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
import SubmitButton from '../../components/Common/SubmitButton';
|
import SubmitButton from '../../components/Common/SubmitButton';
|
||||||
import TextArea from '../../components/Common/TextArea';
|
import TextArea from '../../components/Common/TextArea';
|
||||||
import TextInput from '../../components/Common/TextInput';
|
import TextInput from '../../components/Common/TextInput';
|
||||||
|
@ -76,6 +76,32 @@ function RSFormCard() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
|
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
|
||||||
|
<div className='relative w-full'>
|
||||||
|
<div className='absolute top-0 right-0'>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Поделиться схемой'
|
||||||
|
icon={<ShareIcon size={5} color='text-primary'/>}
|
||||||
|
onClick={shareCurrentURLProc}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Скачать TRS файл'
|
||||||
|
icon={<DownloadIcon size={5} color='text-primary'/>}
|
||||||
|
onClick={handleDownload}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
||||||
|
icon={<CrownIcon size={5} color={isOwned ? '' : 'text-green'}/>}
|
||||||
|
disabled={!isClaimable || !user}
|
||||||
|
onClick={() => { claimOwnershipProc(claim); }}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Удалить схему'
|
||||||
|
disabled={!isEditable}
|
||||||
|
onClick={handleDelete}
|
||||||
|
icon={<DumpBinIcon size={5} color={isEditable ? 'text-red' : ''} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<TextInput id='title' label='Полное название' type='text'
|
<TextInput id='title' label='Полное название' type='text'
|
||||||
required
|
required
|
||||||
value={title}
|
value={title}
|
||||||
|
@ -107,33 +133,6 @@ function RSFormCard() {
|
||||||
disabled={!isModified || !isEditable}
|
disabled={!isModified || !isEditable}
|
||||||
icon={<SaveIcon size={6} />}
|
icon={<SaveIcon size={6} />}
|
||||||
/>
|
/>
|
||||||
<div className='flex justify-end gap-1'>
|
|
||||||
<Button
|
|
||||||
tooltip='Поделиться схемой'
|
|
||||||
icon={<ShareIcon color='text-primary'/>}
|
|
||||||
onClick={shareCurrentURLProc}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip='Скачать TRS файл'
|
|
||||||
icon={<DownloadIcon color='text-primary'/>}
|
|
||||||
loading={processing}
|
|
||||||
onClick={handleDownload}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
|
||||||
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
|
|
||||||
loading={processing}
|
|
||||||
disabled={!isClaimable || !user}
|
|
||||||
onClick={() => { claimOwnershipProc(claim); }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
|
|
||||||
icon={<DumpBinIcon color={isEditable ? 'text-red' : ''} />}
|
|
||||||
loading={processing}
|
|
||||||
disabled={!isEditable}
|
|
||||||
onClick={handleDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-start mt-2'>
|
<div className='flex justify-start mt-2'>
|
||||||
|
|
|
@ -11,23 +11,23 @@ import ConstituentEditor from './ConstituentEditor';
|
||||||
import ConstituentsTable from './ConstituentsTable';
|
import ConstituentsTable from './ConstituentsTable';
|
||||||
import RSFormCard from './RSFormCard';
|
import RSFormCard from './RSFormCard';
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
import TablistTools from './TablistTools';
|
import RSTabsMenu from './RSTabsMenu';
|
||||||
|
|
||||||
export enum RSFormTabsList {
|
export enum RSTabsList {
|
||||||
CARD = 0,
|
CARD = 0,
|
||||||
CST_LIST = 1,
|
CST_LIST = 1,
|
||||||
CST_EDIT = 2
|
CST_EDIT = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSFormTabs() {
|
function RSTabs() {
|
||||||
const { setActiveID, activeID, error, schema, loading } = useRSForm();
|
const { setActiveID, activeID, error, schema, loading } = useRSForm();
|
||||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD);
|
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
|
||||||
const [init, setInit] = useState(false);
|
const [init, setInit] = useState(false);
|
||||||
|
|
||||||
const onEditCst = (cst: IConstituenta) => {
|
const onEditCst = (cst: IConstituenta) => {
|
||||||
console.log(`Set active cst: ${cst.alias}`);
|
console.log(`Set active cst: ${cst.alias}`);
|
||||||
setActiveID(cst.id);
|
setActiveID(cst.id);
|
||||||
setTabIndex(RSFormTabsList.CST_EDIT)
|
setTabIndex(RSTabsList.CST_EDIT)
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectTab = (index: number) => {
|
const onSelectTab = (index: number) => {
|
||||||
|
@ -47,7 +47,7 @@ function RSFormTabs() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const tabQuery = url.searchParams.get('tab');
|
const tabQuery = url.searchParams.get('tab');
|
||||||
setTabIndex(Number(tabQuery) || RSFormTabsList.CARD);
|
setTabIndex(Number(tabQuery) || RSTabsList.CARD);
|
||||||
}, [setTabIndex]);
|
}, [setTabIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -55,7 +55,7 @@ function RSFormTabs() {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const currentActive = url.searchParams.get('active');
|
const currentActive = url.searchParams.get('active');
|
||||||
const currentTab = url.searchParams.get('tab');
|
const currentTab = url.searchParams.get('tab');
|
||||||
const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(activeID);
|
const saveHistory = tabIndex === RSTabsList.CST_EDIT && currentActive !== String(activeID);
|
||||||
if (currentTab !== String(tabIndex)) {
|
if (currentTab !== String(tabIndex)) {
|
||||||
url.searchParams.set('tab', String(tabIndex));
|
url.searchParams.set('tab', String(tabIndex));
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ function RSFormTabs() {
|
||||||
selectedTabClassName='font-bold'
|
selectedTabClassName='font-bold'
|
||||||
>
|
>
|
||||||
<TabList className='flex items-start w-fit clr-bg-pop'>
|
<TabList className='flex items-start w-fit clr-bg-pop'>
|
||||||
<TablistTools />
|
<RSTabsMenu />
|
||||||
<ConceptTab>Паспорт схемы</ConceptTab>
|
<ConceptTab>Паспорт схемы</ConceptTab>
|
||||||
<ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'>
|
<ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'>
|
||||||
<span>Конституенты</span>
|
<span>Конституенты</span>
|
||||||
|
@ -112,4 +112,4 @@ function RSFormTabs() {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RSFormTabs;
|
export default RSTabs;
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@ import { useAuth } from '../../context/AuthContext';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import useDropdown from '../../hooks/useDropdown';
|
import useDropdown from '../../hooks/useDropdown';
|
||||||
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
|
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
|
||||||
|
import UploadRSFormModal from './UploadRSFormModal';
|
||||||
|
|
||||||
function TablistTools() {
|
function RSTabsMenu() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const {
|
const {
|
||||||
|
@ -23,6 +24,7 @@ function TablistTools() {
|
||||||
} = useRSForm();
|
} = useRSForm();
|
||||||
const schemaMenu = useDropdown();
|
const schemaMenu = useDropdown();
|
||||||
const editMenu = useDropdown();
|
const editMenu = useDropdown();
|
||||||
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
|
|
||||||
const handleClaimOwner = useCallback(() => {
|
const handleClaimOwner = useCallback(() => {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
|
@ -41,9 +43,8 @@ function TablistTools() {
|
||||||
}, [schemaMenu, download, schema?.alias]);
|
}, [schemaMenu, download, schema?.alias]);
|
||||||
|
|
||||||
const handleUpload = useCallback(() => {
|
const handleUpload = useCallback(() => {
|
||||||
// TODO: implement
|
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
toast.info('Замена содержимого на файл Экстеора');
|
setShowUploadModal(true);
|
||||||
}, [schemaMenu]);
|
}, [schemaMenu]);
|
||||||
|
|
||||||
const handleClone = useCallback(() => {
|
const handleClone = useCallback(() => {
|
||||||
|
@ -58,6 +59,11 @@ function TablistTools() {
|
||||||
}, [schemaMenu]);
|
}, [schemaMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<UploadRSFormModal
|
||||||
|
show={showUploadModal}
|
||||||
|
hideWindow={() => { setShowUploadModal(false); }}
|
||||||
|
/>
|
||||||
<div className='flex items-center w-fit'>
|
<div className='flex items-center w-fit'>
|
||||||
<div ref={schemaMenu.ref}>
|
<div ref={schemaMenu.ref}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -142,7 +148,8 @@ function TablistTools() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TablistTools
|
export default RSTabsMenu
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import Checkbox from '../../components/Common/Checkbox';
|
||||||
|
import FileInput from '../../components/Common/FileInput';
|
||||||
|
import Modal from '../../components/Common/Modal';
|
||||||
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
|
import { IRSFormUploadData } from '../../utils/models';
|
||||||
|
|
||||||
|
interface UploadRSFormModalProps {
|
||||||
|
show: boolean
|
||||||
|
hideWindow: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadRSFormModal({ show, hideWindow }: UploadRSFormModalProps) {
|
||||||
|
const { upload } = useRSForm();
|
||||||
|
const [loadMetadata, setLoadMetadata] = useState(false);
|
||||||
|
const [file, setFile] = useState<File | undefined>()
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
hideWindow();
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data: IRSFormUploadData = {
|
||||||
|
load_metadata: loadMetadata,
|
||||||
|
file: file,
|
||||||
|
fileName: file.name
|
||||||
|
};
|
||||||
|
upload(data, () => toast.success('Схема загружена из файла'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
|
setFile(event.target.files[0]);
|
||||||
|
} else {
|
||||||
|
setFile(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title='Загрузка схемы из Экстеор'
|
||||||
|
show={show}
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
canSubmit={!!file}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div className='max-w-[20rem]'>
|
||||||
|
<FileInput label='Загрузить файл'
|
||||||
|
acceptType='.trs'
|
||||||
|
onChange={handleFile}
|
||||||
|
/>
|
||||||
|
<Checkbox label='Загружать метаданные'
|
||||||
|
value={loadMetadata}
|
||||||
|
onChange={event => { setLoadMetadata(event.target.checked); }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadRSFormModal;
|
|
@ -1,13 +1,13 @@
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { RSFormState } from '../../context/RSFormContext';
|
import { RSFormState } from '../../context/RSFormContext';
|
||||||
import RSFormTabs from './RSFormTabs';
|
import RSTabs from './RSTabs';
|
||||||
|
|
||||||
function RSFormPage() {
|
function RSFormPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
return (
|
return (
|
||||||
<RSFormState schemaID={id ?? ''}>
|
<RSFormState schemaID={id ?? ''}>
|
||||||
<RSFormTabs />
|
<RSTabs />
|
||||||
</RSFormState>
|
</RSFormState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,10 @@ import {
|
||||||
ExpressionParse,
|
ExpressionParse,
|
||||||
IConstituentaList,
|
IConstituentaList,
|
||||||
IConstituentaMeta,
|
IConstituentaMeta,
|
||||||
ICstCreateData,
|
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstUpdateData,
|
||||||
ICstCreatedResponse,
|
|
||||||
ICstMovetoData,
|
|
||||||
ICstUpdateData,
|
|
||||||
ICurrentUser, IRSFormCreateData, IRSFormData,
|
ICurrentUser, IRSFormCreateData, IRSFormData,
|
||||||
IRSFormMeta, IRSFormUpdateData, IUserInfo, IUserLoginData, IUserProfile, IUserSignupData, RSExpression
|
IRSFormMeta, IRSFormUpdateData, IRSFormUploadData, IUserInfo,
|
||||||
|
IUserLoginData, IUserProfile, IUserSignupData, RSExpression
|
||||||
} from './models'
|
} from './models'
|
||||||
|
|
||||||
// ================ Data transfer types ================
|
// ================ Data transfer types ================
|
||||||
|
@ -203,9 +201,30 @@ export function postCheckExpression(schema: string, request: FrontExchange<RSExp
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
|
||||||
|
AxiosPatch({
|
||||||
|
title: `Reset alias for RSForm id=${target}`,
|
||||||
|
endpoint: `${config.url.BASE}rsforms/${target}/reset-aliases/`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
|
||||||
|
AxiosPatch({
|
||||||
|
title: `Replacing data with trs file for RSForm id=${target}`,
|
||||||
|
endpoint: `${config.url.BASE}rsforms/${target}/load-trs/`,
|
||||||
|
request: request,
|
||||||
|
options: {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Helper functions =============
|
// ============ Helper functions =============
|
||||||
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
|
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
|
||||||
console.log(`[[${title}]] requested`);
|
console.log(`REQUEST: [[${title}]]`);
|
||||||
if (request.setLoading) request?.setLoading(true);
|
if (request.setLoading) request?.setLoading(true);
|
||||||
axios.get<ResponseData>(endpoint, options)
|
axios.get<ResponseData>(endpoint, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@ -222,7 +241,7 @@ function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosReq
|
||||||
function AxiosPost<RequestData, ResponseData>(
|
function AxiosPost<RequestData, ResponseData>(
|
||||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||||
) {
|
) {
|
||||||
console.log(`[[${title}]] posted`);
|
console.log(`POST: [[${title}]]`);
|
||||||
if (request.setLoading) request.setLoading(true);
|
if (request.setLoading) request.setLoading(true);
|
||||||
axios.post<ResponseData>(endpoint, request.data, options)
|
axios.post<ResponseData>(endpoint, request.data, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@ -239,7 +258,7 @@ function AxiosPost<RequestData, ResponseData>(
|
||||||
function AxiosDelete<RequestData, ResponseData>(
|
function AxiosDelete<RequestData, ResponseData>(
|
||||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||||
) {
|
) {
|
||||||
console.log(`[[${title}]] is being deleted`);
|
console.log(`DELETE: [[${title}]]`);
|
||||||
if (request.setLoading) request.setLoading(true);
|
if (request.setLoading) request.setLoading(true);
|
||||||
axios.delete<ResponseData>(endpoint, options)
|
axios.delete<ResponseData>(endpoint, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
@ -256,7 +275,7 @@ function AxiosDelete<RequestData, ResponseData>(
|
||||||
function AxiosPatch<RequestData, ResponseData>(
|
function AxiosPatch<RequestData, ResponseData>(
|
||||||
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
|
||||||
) {
|
) {
|
||||||
console.log(`[[${title}]] is being patrially updated`);
|
console.log(`PATCH: [[${title}]]`);
|
||||||
if (request.setLoading) request.setLoading(true);
|
if (request.setLoading) request.setLoading(true);
|
||||||
axios.patch<ResponseData>(endpoint, request.data, options)
|
axios.patch<ResponseData>(endpoint, request.data, options)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|
|
@ -179,6 +179,12 @@ extends IRSFormUpdateData {
|
||||||
fileName?: string
|
fileName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRSFormUploadData {
|
||||||
|
load_metadata: boolean
|
||||||
|
file: File
|
||||||
|
fileName: string
|
||||||
|
}
|
||||||
|
|
||||||
// ================ Misc types ================
|
// ================ Misc types ================
|
||||||
// Constituenta edit mode
|
// Constituenta edit mode
|
||||||
export enum EditMode {
|
export enum EditMode {
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function claimOwnershipProc(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteRSFormProc(
|
export function deleteRSFormProc(
|
||||||
destroy: (callback: DataCallback) => void,
|
destroy: (callback?: () => void) => void,
|
||||||
navigate: (path: string) => void
|
navigate: (path: string) => void
|
||||||
) {
|
) {
|
||||||
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
|
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user