mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Improve RSForm edit UI
This commit is contained in:
parent
f26ba55fef
commit
b8b8143b51
|
@ -1,6 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import RSForm
|
from .models import Constituenta, RSForm
|
||||||
|
|
||||||
|
|
||||||
class FileSerializer(serializers.Serializer):
|
class FileSerializer(serializers.Serializer):
|
||||||
|
@ -16,3 +16,14 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
model = RSForm
|
model = RSForm
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = ('owner', 'id')
|
read_only_fields = ('owner', 'id')
|
||||||
|
|
||||||
|
|
||||||
|
class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Constituenta
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ('id', 'order', 'alias', 'csttype')
|
||||||
|
|
||||||
|
def update(self, instance: Constituenta, validated_data):
|
||||||
|
instance.schema.save()
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from rest_framework.test import APITestCase, APIRequestFactory, APIClient
|
||||||
from rest_framework.exceptions import ErrorDetail
|
from rest_framework.exceptions import ErrorDetail
|
||||||
|
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from apps.rsform.models import Syntax, RSForm, CstType
|
from apps.rsform.models import Syntax, RSForm, Constituenta, CstType
|
||||||
from apps.rsform.views import (
|
from apps.rsform.views import (
|
||||||
convert_to_ascii,
|
convert_to_ascii,
|
||||||
convert_to_math,
|
convert_to_math,
|
||||||
|
@ -15,6 +15,53 @@ from apps.rsform.views import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConstituentaAPI(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = APIRequestFactory()
|
||||||
|
self.user = User.objects.create(username='UserTest')
|
||||||
|
self.client = APIClient()
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
self.rsform_owned: RSForm = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
|
||||||
|
self.rsform_unowned: RSForm = RSForm.objects.create(title='Test2', alias='T2')
|
||||||
|
self.cst1 = Constituenta.objects.create(
|
||||||
|
alias='X1', schema=self.rsform_owned, order=1, convention='Test')
|
||||||
|
self.cst2 = Constituenta.objects.create(
|
||||||
|
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1')
|
||||||
|
|
||||||
|
def test_retrieve(self):
|
||||||
|
response = self.client.get(f'/api/constituents/{self.cst1.id}/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data['alias'], self.cst1.alias)
|
||||||
|
self.assertEqual(response.data['convention'], self.cst1.convention)
|
||||||
|
|
||||||
|
def test_partial_update(self):
|
||||||
|
data = json.dumps({'convention': 'tt'})
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst2.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.cst1.refresh_from_db()
|
||||||
|
self.assertEqual(response.data['convention'], 'tt')
|
||||||
|
self.assertEqual(self.cst1.convention, 'tt')
|
||||||
|
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_readonly_cst_fields(self):
|
||||||
|
data = json.dumps({'alias': 'X33', 'order': 10})
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data['alias'], 'X1')
|
||||||
|
self.assertEqual(response.data['alias'], self.cst1.alias)
|
||||||
|
self.assertEqual(response.data['order'], self.cst1.order)
|
||||||
|
|
||||||
|
|
||||||
class TestRSFormViewset(APITestCase):
|
class TestRSFormViewset(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
|
|
|
@ -7,6 +7,7 @@ rsform_router = routers.SimpleRouter()
|
||||||
rsform_router.register(r'rsforms', views.RSFormViewSet)
|
rsform_router.register(r'rsforms', views.RSFormViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path('constituents/<int:pk>/', views.ConstituentAPIView.as_view()),
|
||||||
path('rsforms/import-trs/', views.TrsImportView.as_view()),
|
path('rsforms/import-trs/', views.TrsImportView.as_view()),
|
||||||
path('rsforms/create-detailed/', views.create_rsform),
|
path('rsforms/create-detailed/', views.create_rsform),
|
||||||
path('func/parse-expression/', views.parse_expression),
|
path('func/parse-expression/', views.parse_expression),
|
||||||
|
|
|
@ -11,6 +11,12 @@ class ObjectOwnerOrAdmin(BasePermission):
|
||||||
return request.user == obj.owner or request.user.is_staff
|
return request.user == obj.owner or request.user.is_staff
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaOwnerOrAdmin(BasePermission):
|
||||||
|
''' Permission for object ownership restriction '''
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
return request.user == obj.schema.owner or request.user.is_staff
|
||||||
|
|
||||||
|
|
||||||
def read_trs(file) -> dict:
|
def read_trs(file) -> dict:
|
||||||
''' Read JSON from TRS file '''
|
''' Read JSON from TRS file '''
|
||||||
# TODO: deal with different versions
|
# TODO: deal with different versions
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import json
|
import json
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework import views, viewsets, filters, generics, permissions
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework import views, viewsets, filters
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework import permissions
|
|
||||||
|
|
||||||
import pyconcept
|
import pyconcept
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -13,6 +12,19 @@ from . import serializers
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
||||||
|
queryset = models.Constituenta.objects.all()
|
||||||
|
serializer_class = serializers.ConstituentaSerializer
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
result = super().get_permissions()
|
||||||
|
if self.request.method.lower() == 'get':
|
||||||
|
result.append(permissions.AllowAny())
|
||||||
|
else:
|
||||||
|
result.append(utils.SchemaOwnerOrAdmin())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class RSFormViewSet(viewsets.ModelViewSet):
|
class RSFormViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.RSForm.objects.all()
|
queryset = models.RSForm.objects.all()
|
||||||
serializer_class = serializers.RSFormSerializer
|
serializer_class = serializers.RSFormSerializer
|
||||||
|
|
6
rsconcept/frontend/package-lock.json
generated
6
rsconcept/frontend/package-lock.json
generated
|
@ -16,6 +16,7 @@
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-data-table-component": "^7.5.3",
|
"react-data-table-component": "^7.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -11872,6 +11873,11 @@
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-file-download": {
|
||||||
|
"version": "0.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz",
|
||||||
|
"integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg=="
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-data-table-component": "^7.5.3",
|
"react-data-table-component": "^7.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
@ -109,6 +109,15 @@ export async function patchRSForm(target: string, request?: IFrontRequest) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function patchConstituenta(target: string, request?: IFrontRequest) {
|
||||||
|
AxiosPatch({
|
||||||
|
title: `Constituenta id=${target}`,
|
||||||
|
endpoint: `${config.url.BASE}constituents/${target}/`,
|
||||||
|
request: request
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function deleteRSForm(target: string, request?: IFrontRequest) {
|
export async function deleteRSForm(target: string, request?: IFrontRequest) {
|
||||||
AxiosDelete({
|
AxiosDelete({
|
||||||
title: `RSForm id=${target}`,
|
title: `RSForm id=${target}`,
|
||||||
|
|
|
@ -4,9 +4,9 @@ interface CardProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function Card({title, widthClass='w-fit', children}: CardProps) {
|
function Card({title, widthClass='min-w-fit', children}: CardProps) {
|
||||||
return (
|
return (
|
||||||
<div className={'border shadow-md py-2 bg-gray-50 dark:bg-gray-600 px-6 ' + widthClass}>
|
<div className={`border shadow-md py-2 bg-gray-50 dark:bg-gray-600 px-6 ${widthClass}`}>
|
||||||
{ title && <h1 className='mb-2 text-xl font-bold'>{title}</h1> }
|
{ title && <h1 className='mb-2 text-xl font-bold'>{title}</h1> }
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
10
rsconcept/frontend/src/components/Common/Divider.tsx
Normal file
10
rsconcept/frontend/src/components/Common/Divider.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
function Divider() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='my-2 border-b'
|
||||||
|
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Divider;
|
27
rsconcept/frontend/src/components/Common/LabeledText.tsx
Normal file
27
rsconcept/frontend/src/components/Common/LabeledText.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
interface LabeledTextProps {
|
||||||
|
id?: string
|
||||||
|
label: string
|
||||||
|
text: any
|
||||||
|
tooltip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabeledText({id, label, text, tooltip}: LabeledTextProps) {
|
||||||
|
return (
|
||||||
|
<div className='flex justify-between gap-2'>
|
||||||
|
<label
|
||||||
|
className='font-semibold'
|
||||||
|
title={tooltip}
|
||||||
|
htmlFor={id}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LabeledText;
|
|
@ -3,7 +3,7 @@ import { IConstituenta, IRSForm } from '../models';
|
||||||
import { useRSFormDetails } from '../hooks/useRSFormDetails';
|
import { useRSFormDetails } from '../hooks/useRSFormDetails';
|
||||||
import { ErrorInfo } from '../components/BackendError';
|
import { ErrorInfo } from '../components/BackendError';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { BackendCallback, deleteRSForm, getTRSFile, patchRSForm, postClaimRSForm } from '../backendAPI';
|
import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm } from '../backendAPI';
|
||||||
|
|
||||||
interface IRSFormContext {
|
interface IRSFormContext {
|
||||||
schema?: IRSForm
|
schema?: IRSForm
|
||||||
|
@ -20,6 +20,8 @@ interface IRSFormContext {
|
||||||
destroy: (callback: BackendCallback) => void
|
destroy: (callback: BackendCallback) => void
|
||||||
claim: (callback: BackendCallback) => void
|
claim: (callback: BackendCallback) => void
|
||||||
download: (callback: BackendCallback) => void
|
download: (callback: BackendCallback) => void
|
||||||
|
|
||||||
|
cstUpdate: (data: any, callback: BackendCallback) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RSFormContext = createContext<IRSFormContext>({
|
export const RSFormContext = createContext<IRSFormContext>({
|
||||||
|
@ -37,6 +39,8 @@ export const RSFormContext = createContext<IRSFormContext>({
|
||||||
destroy: () => {},
|
destroy: () => {},
|
||||||
claim: () => {},
|
claim: () => {},
|
||||||
download: () => {},
|
download: () => {},
|
||||||
|
|
||||||
|
cstUpdate: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
interface RSFormStateProps {
|
interface RSFormStateProps {
|
||||||
|
@ -94,11 +98,23 @@ export const RSFormState = ({ id, children }: RSFormStateProps) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function cstUpdate(data: any, callback?: BackendCallback) {
|
||||||
|
setError(undefined);
|
||||||
|
patchConstituenta(String(active!.entityUID), {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSucccess: callback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RSFormContext.Provider value={{
|
<RSFormContext.Provider value={{
|
||||||
schema, error, loading, processing,
|
schema, error, loading, processing,
|
||||||
active, setActive,
|
active, setActive,
|
||||||
isEditable, isClaimable,
|
isEditable, isClaimable,
|
||||||
|
cstUpdate,
|
||||||
reload, update, download, destroy, claim
|
reload, update, download, destroy, claim
|
||||||
}}>
|
}}>
|
||||||
{ children }
|
{ children }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { IRSForm } from '../models'
|
import { CalculateStats, IRSForm } from '../models'
|
||||||
import { ErrorInfo } from '../components/BackendError';
|
import { ErrorInfo } from '../components/BackendError';
|
||||||
import { getRSFormDetails } from '../backendAPI';
|
import { getRSFormDetails } from '../backendAPI';
|
||||||
|
|
||||||
|
@ -18,7 +18,10 @@ export function useRSFormDetails({target}: {target?: string}) {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
onError: error => setError(error),
|
onError: error => setError(error),
|
||||||
onSucccess: response => setSchema(response.data)
|
onSucccess: (response) => {
|
||||||
|
CalculateStats(response.data)
|
||||||
|
setSchema(response.data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [target]);
|
}, [target]);
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ export interface IConstituenta {
|
||||||
term?: {
|
term?: {
|
||||||
raw: string
|
raw: string
|
||||||
resolved?: string
|
resolved?: string
|
||||||
|
forms?: string[]
|
||||||
}
|
}
|
||||||
definition?: {
|
definition?: {
|
||||||
formal: string
|
formal: string
|
||||||
|
@ -83,13 +84,32 @@ export interface IConstituenta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parse?: {
|
parse?: {
|
||||||
status: string
|
status: ParsingStatus
|
||||||
valueClass: ValueClass
|
valueClass: ValueClass
|
||||||
typification: string
|
typification: string
|
||||||
syntaxTree: string
|
syntaxTree: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RSForm stats
|
||||||
|
export interface IRSFormStats {
|
||||||
|
count_all: number
|
||||||
|
count_errors: number
|
||||||
|
count_property: number
|
||||||
|
count_incalc: number
|
||||||
|
|
||||||
|
count_termin: number
|
||||||
|
|
||||||
|
count_base: number
|
||||||
|
count_constant: number
|
||||||
|
count_structured: number
|
||||||
|
count_axiom: number
|
||||||
|
count_term: number
|
||||||
|
count_function: number
|
||||||
|
count_predicate: number
|
||||||
|
count_theorem: number
|
||||||
|
}
|
||||||
|
|
||||||
// RSForm data
|
// RSForm data
|
||||||
export interface IRSForm {
|
export interface IRSForm {
|
||||||
id: number
|
id: number
|
||||||
|
@ -101,9 +121,10 @@ export interface IRSForm {
|
||||||
time_update: string
|
time_update: string
|
||||||
owner?: number
|
owner?: number
|
||||||
items?: IConstituenta[]
|
items?: IConstituenta[]
|
||||||
|
stats?: IRSFormStats
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSForm data
|
// RSForm user input
|
||||||
export interface IRSFormCreateData {
|
export interface IRSFormCreateData {
|
||||||
title: string
|
title: string
|
||||||
alias: string
|
alias: string
|
||||||
|
@ -153,3 +174,92 @@ export function GetCstTypeLabel(type: CstType) {
|
||||||
case CstType.THEOREM: return 'Теорема';
|
case CstType.THEOREM: return 'Теорема';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CalculateStats(schema: IRSForm) {
|
||||||
|
if (!schema.items) {
|
||||||
|
schema.stats = {
|
||||||
|
count_all: 0,
|
||||||
|
count_errors: 0,
|
||||||
|
count_property: 0,
|
||||||
|
count_incalc: 0,
|
||||||
|
|
||||||
|
count_termin: 0,
|
||||||
|
|
||||||
|
count_base: 0,
|
||||||
|
count_constant: 0,
|
||||||
|
count_structured: 0,
|
||||||
|
count_axiom: 0,
|
||||||
|
count_term: 0,
|
||||||
|
count_function: 0,
|
||||||
|
count_predicate: 0,
|
||||||
|
count_theorem: 0,
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
schema.stats = {
|
||||||
|
count_all: schema.items?.length || 0,
|
||||||
|
count_errors: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.parse?.status === ParsingStatus.INCORRECT ? 1 : 0) || 0,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_property: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.parse?.valueClass === ValueClass.PROPERTY ? 1 : 0) || 0,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_incalc: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
((cst.parse?.status === ParsingStatus.VERIFIED &&
|
||||||
|
cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
|
||||||
|
count_termin: schema.items?.reduce(
|
||||||
|
(sum, cst) => (sum +
|
||||||
|
(cst.term?.raw ? 1 : 0) || 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
|
||||||
|
count_base: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.BASE ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_constant: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.CONSTANT ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_structured: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.STRUCTURED ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_axiom: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.AXIOM ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_term: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.TERM ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_function: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.FUNCTION ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_predicate: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.PREDICATE ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
count_theorem: schema.items?.reduce(
|
||||||
|
(sum, cst) => sum +
|
||||||
|
(cst.cstType === CstType.THEOREM ? 1 : 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,9 @@ import ExpressionEditor from './ExpressionEditor';
|
||||||
import SubmitButton from '../../components/Common/SubmitButton';
|
import SubmitButton from '../../components/Common/SubmitButton';
|
||||||
|
|
||||||
function ConstituentEditor() {
|
function ConstituentEditor() {
|
||||||
const { active, schema, setActive, isEditable } = useRSForm();
|
const {
|
||||||
|
active, schema, setActive, processing, cstUpdate, isEditable, reload
|
||||||
|
} = useRSForm();
|
||||||
|
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
const [type, setType] = useState('');
|
const [type, setType] = useState('');
|
||||||
|
@ -27,6 +29,7 @@ function ConstituentEditor() {
|
||||||
if (active) {
|
if (active) {
|
||||||
setAlias(active.alias);
|
setAlias(active.alias);
|
||||||
setType(GetCstTypeLabel(active.cstType));
|
setType(GetCstTypeLabel(active.cstType));
|
||||||
|
setConvention(active.convention || '');
|
||||||
setTerm(active.term?.raw || '');
|
setTerm(active.term?.raw || '');
|
||||||
setTextDefinition(active.definition?.text?.raw || '');
|
setTextDefinition(active.definition?.text?.raw || '');
|
||||||
setExpression(active.definition?.formal || '');
|
setExpression(active.definition?.formal || '');
|
||||||
|
@ -35,18 +38,27 @@ function ConstituentEditor() {
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// if (!processing) {
|
if (!processing) {
|
||||||
// const data = {
|
const data = {
|
||||||
// 'title': title,
|
'alias': alias,
|
||||||
// 'alias': alias,
|
'convention': convention,
|
||||||
// 'comment': comment,
|
'definition_formal': expression,
|
||||||
// 'is_common': common,
|
'definition_text': {
|
||||||
// };
|
'raw': textDefinition,
|
||||||
// upload(data, () => {
|
'resolved': '',
|
||||||
// toast.success('Изменения сохранены');
|
},
|
||||||
// reload();
|
'term': {
|
||||||
// });
|
'raw': term,
|
||||||
// }
|
'resolved': '',
|
||||||
|
'forms': active?.term?.forms || [],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cstUpdate(data, (response) => {
|
||||||
|
console.log(response);
|
||||||
|
toast.success('Изменения сохранены');
|
||||||
|
reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRename = useCallback(() => {
|
const handleRename = useCallback(() => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { CrownIcon, DownloadIcon, DumpBinIcon, ShareIcon } from '../../component
|
||||||
import { useUsers } from '../../context/UsersContext';
|
import { useUsers } from '../../context/UsersContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import fileDownload from 'js-file-download';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
function RSFormCard() {
|
function RSFormCard() {
|
||||||
|
@ -23,10 +24,6 @@ function RSFormCard() {
|
||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
const [common, setCommon] = useState(false);
|
const [common, setCommon] = useState(false);
|
||||||
|
|
||||||
const fileRef = useRef<HTMLAnchorElement | null>(null);
|
|
||||||
const [fileURL, setFileUrl] = useState<string>();
|
|
||||||
const [fileName, setFileName] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(schema!.title)
|
setTitle(schema!.title)
|
||||||
setAlias(schema!.alias)
|
setAlias(schema!.alias)
|
||||||
|
@ -69,15 +66,13 @@ function RSFormCard() {
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
download((response: AxiosResponse) => {
|
download((response: AxiosResponse) => {
|
||||||
try {
|
try {
|
||||||
setFileName((schema?.alias || 'Schema') + '.trs')
|
const fileName = (schema?.alias || 'Schema') + '.trs';
|
||||||
setFileUrl(URL.createObjectURL(new Blob([response.data])));
|
fileDownload(response.data, fileName);
|
||||||
fileRef.current?.click();
|
|
||||||
if (fileURL) URL.revokeObjectURL(fileURL);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [download, schema?.alias, fileURL]);
|
}, [download, schema?.alias]);
|
||||||
|
|
||||||
const handleShare = useCallback(() => {
|
const handleShare = useCallback(() => {
|
||||||
const url = window.location.href + '&share';
|
const url = window.location.href + '&share';
|
||||||
|
@ -86,7 +81,7 @@ function RSFormCard() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border'>
|
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
|
||||||
<TextInput id='title' label='Полное название' type='text'
|
<TextInput id='title' label='Полное название' type='text'
|
||||||
required
|
required
|
||||||
value={title}
|
value={title}
|
||||||
|
@ -126,9 +121,6 @@ function RSFormCard() {
|
||||||
loading={processing}
|
loading={processing}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
/>
|
/>
|
||||||
<a href={fileURL} download={fileName} className='hidden' ref={fileRef}>
|
|
||||||
<i aria-hidden="true"/>
|
|
||||||
</a>
|
|
||||||
<Button
|
<Button
|
||||||
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
||||||
disabled={!isClaimable || processing}
|
disabled={!isClaimable || processing}
|
||||||
|
|
|
@ -1,17 +1,79 @@
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
|
||||||
import Card from '../../components/Common/Card';
|
import Card from '../../components/Common/Card';
|
||||||
import PrettyJson from '../../components/Common/PrettyJSON';
|
import Divider from '../../components/Common/Divider';
|
||||||
|
import LabeledText from '../../components/Common/LabeledText';
|
||||||
|
import { IRSFormStats } from '../../models';
|
||||||
|
|
||||||
function RSFormStats() {
|
interface RSFormStatsProps {
|
||||||
const { schema } = useRSForm();
|
stats: IRSFormStats
|
||||||
|
}
|
||||||
|
|
||||||
|
function RSFormStats({stats}: RSFormStatsProps) {
|
||||||
return (
|
return (
|
||||||
<Card widthClass='max-w-sm flex-grow'>
|
<Card>
|
||||||
<div className='flex justify-start'>
|
<LabeledText id='count_all'
|
||||||
<label className='font-semibold'>Всего конституент:</label>
|
label='Всего конституент '
|
||||||
<span className='ml-2'>{schema!.items!.length}</span>
|
text={stats.count_all}
|
||||||
</div>
|
/>
|
||||||
<PrettyJson data={schema || ''}/>
|
<LabeledText id='count_errors'
|
||||||
|
label='Ошибок '
|
||||||
|
text={stats.count_errors}
|
||||||
|
/>
|
||||||
|
{ stats.count_property > 0 &&
|
||||||
|
<LabeledText id='count_property'
|
||||||
|
label='Только свойство '
|
||||||
|
text={stats.count_property}
|
||||||
|
/>}
|
||||||
|
{ stats.count_incalc > 0 &&
|
||||||
|
<LabeledText id='count_incalc'
|
||||||
|
label='Невычислимы '
|
||||||
|
text={stats.count_incalc}
|
||||||
|
/>}
|
||||||
|
<Divider />
|
||||||
|
<LabeledText id='count_termin'
|
||||||
|
label='Термины '
|
||||||
|
text={stats.count_termin}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
{ stats.count_base > 0 &&
|
||||||
|
<LabeledText id='count_base'
|
||||||
|
label='Базисные множества '
|
||||||
|
text={stats.count_base}
|
||||||
|
/>}
|
||||||
|
{ stats.count_constant > 0 &&
|
||||||
|
<LabeledText id='count_constant'
|
||||||
|
label='Константные множества '
|
||||||
|
text={stats.count_constant}
|
||||||
|
/>}
|
||||||
|
{ stats.count_structured > 0 &&
|
||||||
|
<LabeledText id='count_structured'
|
||||||
|
label='Родовые структуры '
|
||||||
|
text={stats.count_structured}
|
||||||
|
/>}
|
||||||
|
{ stats.count_axiom > 0 &&
|
||||||
|
<LabeledText id='count_axiom'
|
||||||
|
label='Аксиомы '
|
||||||
|
text={stats.count_axiom}
|
||||||
|
/>}
|
||||||
|
{ stats.count_term > 0 &&
|
||||||
|
<LabeledText id='count_term'
|
||||||
|
label='Термы '
|
||||||
|
text={stats.count_term}
|
||||||
|
/>}
|
||||||
|
{ stats.count_function > 0 &&
|
||||||
|
<LabeledText id='count_function'
|
||||||
|
label='Терм-функции '
|
||||||
|
text={stats.count_function}
|
||||||
|
/>}
|
||||||
|
{ stats.count_predicate > 0 &&
|
||||||
|
<LabeledText id='count_predicate'
|
||||||
|
label='Предикат-функции '
|
||||||
|
text={stats.count_predicate}
|
||||||
|
/>}
|
||||||
|
{ stats.count_theorem > 0 &&
|
||||||
|
<LabeledText id='count_theorem'
|
||||||
|
label='Теормы '
|
||||||
|
text={stats.count_theorem}
|
||||||
|
/>}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import BackendError from '../../components/BackendError';
|
||||||
import ConstituentEditor from './ConstituentEditor';
|
import ConstituentEditor from './ConstituentEditor';
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
enum TabsList {
|
enum TabsList {
|
||||||
CARD = 0,
|
CARD = 0,
|
||||||
|
@ -21,7 +20,6 @@ enum TabsList {
|
||||||
function RSFormTabs() {
|
function RSFormTabs() {
|
||||||
const { setActive, active, error, schema, loading } = useRSForm();
|
const { setActive, active, error, schema, loading } = useRSForm();
|
||||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD);
|
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD);
|
||||||
const search = useLocation().search;
|
|
||||||
|
|
||||||
const onEditCst = (cst: IConstituenta) => {
|
const onEditCst = (cst: IConstituenta) => {
|
||||||
console.log(`Set active cst: ${cst.alias}`);
|
console.log(`Set active cst: ${cst.alias}`);
|
||||||
|
@ -34,22 +32,28 @@ function RSFormTabs() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tabQuery = new URLSearchParams(search).get('tab');
|
const url = new URL(window.location.href);
|
||||||
const activeQuery = new URLSearchParams(search).get('active');
|
const activeQuery = url.searchParams.get('active');
|
||||||
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
|
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
|
||||||
setTabIndex(Number(tabQuery) || TabsList.CARD);
|
|
||||||
setActive(activeCst);
|
setActive(activeCst);
|
||||||
}, [search, setTabIndex, setActive, schema?.items]);
|
}, [setActive, schema?.items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema) {
|
const url = new URL(window.location.href);
|
||||||
let url = `/rsforms/${schema.id}?tab=${tabIndex}`
|
const tabQuery = url.searchParams.get('tab');
|
||||||
|
setTabIndex(Number(tabQuery) || TabsList.CARD);
|
||||||
|
}, [setTabIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('tab', String(tabIndex));
|
||||||
if (active) {
|
if (active) {
|
||||||
url = url + `&active=${active.entityUID}`
|
url.searchParams.set('active', String(active.entityUID));
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('active');
|
||||||
}
|
}
|
||||||
window.history.replaceState(null, '', url);
|
window.history.replaceState(null, '', url.toString());
|
||||||
}
|
}, [tabIndex, active]);
|
||||||
}, [tabIndex, active, schema]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container w-full'>
|
<div className='container w-full'>
|
||||||
|
@ -70,7 +74,7 @@ function RSFormTabs() {
|
||||||
|
|
||||||
<TabPanel className='flex items-start w-full gap-2'>
|
<TabPanel className='flex items-start w-full gap-2'>
|
||||||
<RSFormCard />
|
<RSFormCard />
|
||||||
<RSFormStats />
|
{schema.stats && <RSFormStats stats={schema.stats}/>}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel className='w-fit'>
|
<TabPanel className='w-fit'>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user