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 .models import RSForm
|
||||
from .models import Constituenta, RSForm
|
||||
|
||||
|
||||
class FileSerializer(serializers.Serializer):
|
||||
|
@ -16,3 +16,14 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
model = RSForm
|
||||
fields = '__all__'
|
||||
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 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 (
|
||||
convert_to_ascii,
|
||||
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):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
|
|
|
@ -7,6 +7,7 @@ rsform_router = routers.SimpleRouter()
|
|||
rsform_router.register(r'rsforms', views.RSFormViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('constituents/<int:pk>/', views.ConstituentAPIView.as_view()),
|
||||
path('rsforms/import-trs/', views.TrsImportView.as_view()),
|
||||
path('rsforms/create-detailed/', views.create_rsform),
|
||||
path('func/parse-expression/', views.parse_expression),
|
||||
|
|
|
@ -11,6 +11,12 @@ class ObjectOwnerOrAdmin(BasePermission):
|
|||
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:
|
||||
''' Read JSON from TRS file '''
|
||||
# TODO: deal with different versions
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import json
|
||||
from django.http import HttpResponse
|
||||
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 import views, viewsets, filters
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework import permissions
|
||||
|
||||
import pyconcept
|
||||
from . import models
|
||||
|
@ -13,6 +12,19 @@ from . import serializers
|
|||
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):
|
||||
queryset = models.RSForm.objects.all()
|
||||
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-dom": "^18.2.4",
|
||||
"axios": "^1.4.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.2.0",
|
||||
"react-data-table-component": "^7.5.3",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -11872,6 +11873,11 @@
|
|||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"axios": "^1.4.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.2.0",
|
||||
"react-data-table-component": "^7.5.3",
|
||||
"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) {
|
||||
AxiosDelete({
|
||||
title: `RSForm id=${target}`,
|
||||
|
|
|
@ -4,9 +4,9 @@ interface CardProps {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Card({title, widthClass='w-fit', children}: CardProps) {
|
||||
function Card({title, widthClass='min-w-fit', children}: CardProps) {
|
||||
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> }
|
||||
{children}
|
||||
</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 { ErrorInfo } from '../components/BackendError';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { BackendCallback, deleteRSForm, getTRSFile, patchRSForm, postClaimRSForm } from '../backendAPI';
|
||||
import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm } from '../backendAPI';
|
||||
|
||||
interface IRSFormContext {
|
||||
schema?: IRSForm
|
||||
|
@ -20,6 +20,8 @@ interface IRSFormContext {
|
|||
destroy: (callback: BackendCallback) => void
|
||||
claim: (callback: BackendCallback) => void
|
||||
download: (callback: BackendCallback) => void
|
||||
|
||||
cstUpdate: (data: any, callback: BackendCallback) => void
|
||||
}
|
||||
|
||||
export const RSFormContext = createContext<IRSFormContext>({
|
||||
|
@ -37,6 +39,8 @@ export const RSFormContext = createContext<IRSFormContext>({
|
|||
destroy: () => {},
|
||||
claim: () => {},
|
||||
download: () => {},
|
||||
|
||||
cstUpdate: () => {},
|
||||
})
|
||||
|
||||
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 (
|
||||
<RSFormContext.Provider value={{
|
||||
schema, error, loading, processing,
|
||||
active, setActive,
|
||||
isEditable, isClaimable,
|
||||
cstUpdate,
|
||||
reload, update, download, destroy, claim
|
||||
}}>
|
||||
{ children }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { IRSForm } from '../models'
|
||||
import { CalculateStats, IRSForm } from '../models'
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
import { getRSFormDetails } from '../backendAPI';
|
||||
|
||||
|
@ -18,7 +18,10 @@ export function useRSFormDetails({target}: {target?: string}) {
|
|||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: error => setError(error),
|
||||
onSucccess: response => setSchema(response.data)
|
||||
onSucccess: (response) => {
|
||||
CalculateStats(response.data)
|
||||
setSchema(response.data);
|
||||
}
|
||||
});
|
||||
}, [target]);
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ export interface IConstituenta {
|
|||
term?: {
|
||||
raw: string
|
||||
resolved?: string
|
||||
forms?: string[]
|
||||
}
|
||||
definition?: {
|
||||
formal: string
|
||||
|
@ -83,13 +84,32 @@ export interface IConstituenta {
|
|||
}
|
||||
}
|
||||
parse?: {
|
||||
status: string
|
||||
status: ParsingStatus
|
||||
valueClass: ValueClass
|
||||
typification: 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
|
||||
export interface IRSForm {
|
||||
id: number
|
||||
|
@ -101,9 +121,10 @@ export interface IRSForm {
|
|||
time_update: string
|
||||
owner?: number
|
||||
items?: IConstituenta[]
|
||||
stats?: IRSFormStats
|
||||
}
|
||||
|
||||
// RSForm data
|
||||
// RSForm user input
|
||||
export interface IRSFormCreateData {
|
||||
title: string
|
||||
alias: string
|
||||
|
@ -153,3 +174,92 @@ export function GetCstTypeLabel(type: CstType) {
|
|||
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';
|
||||
|
||||
function ConstituentEditor() {
|
||||
const { active, schema, setActive, isEditable } = useRSForm();
|
||||
const {
|
||||
active, schema, setActive, processing, cstUpdate, isEditable, reload
|
||||
} = useRSForm();
|
||||
|
||||
const [alias, setAlias] = useState('');
|
||||
const [type, setType] = useState('');
|
||||
|
@ -27,6 +29,7 @@ function ConstituentEditor() {
|
|||
if (active) {
|
||||
setAlias(active.alias);
|
||||
setType(GetCstTypeLabel(active.cstType));
|
||||
setConvention(active.convention || '');
|
||||
setTerm(active.term?.raw || '');
|
||||
setTextDefinition(active.definition?.text?.raw || '');
|
||||
setExpression(active.definition?.formal || '');
|
||||
|
@ -35,18 +38,27 @@ function ConstituentEditor() {
|
|||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
// if (!processing) {
|
||||
// const data = {
|
||||
// 'title': title,
|
||||
// 'alias': alias,
|
||||
// 'comment': comment,
|
||||
// 'is_common': common,
|
||||
// };
|
||||
// upload(data, () => {
|
||||
// toast.success('Изменения сохранены');
|
||||
// reload();
|
||||
// });
|
||||
// }
|
||||
if (!processing) {
|
||||
const data = {
|
||||
'alias': alias,
|
||||
'convention': convention,
|
||||
'definition_formal': expression,
|
||||
'definition_text': {
|
||||
'raw': textDefinition,
|
||||
'resolved': '',
|
||||
},
|
||||
'term': {
|
||||
'raw': term,
|
||||
'resolved': '',
|
||||
'forms': active?.term?.forms || [],
|
||||
}
|
||||
};
|
||||
cstUpdate(data, (response) => {
|
||||
console.log(response);
|
||||
toast.success('Изменения сохранены');
|
||||
reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { CrownIcon, DownloadIcon, DumpBinIcon, ShareIcon } from '../../component
|
|||
import { useUsers } from '../../context/UsersContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
function RSFormCard() {
|
||||
|
@ -23,10 +24,6 @@ function RSFormCard() {
|
|||
const [comment, setComment] = useState('');
|
||||
const [common, setCommon] = useState(false);
|
||||
|
||||
const fileRef = useRef<HTMLAnchorElement | null>(null);
|
||||
const [fileURL, setFileUrl] = useState<string>();
|
||||
const [fileName, setFileName] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(schema!.title)
|
||||
setAlias(schema!.alias)
|
||||
|
@ -69,15 +66,13 @@ function RSFormCard() {
|
|||
const handleDownload = useCallback(() => {
|
||||
download((response: AxiosResponse) => {
|
||||
try {
|
||||
setFileName((schema?.alias || 'Schema') + '.trs')
|
||||
setFileUrl(URL.createObjectURL(new Blob([response.data])));
|
||||
fileRef.current?.click();
|
||||
if (fileURL) URL.revokeObjectURL(fileURL);
|
||||
const fileName = (schema?.alias || 'Schema') + '.trs';
|
||||
fileDownload(response.data, fileName);
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
});
|
||||
}, [download, schema?.alias, fileURL]);
|
||||
}, [download, schema?.alias]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const url = window.location.href + '&share';
|
||||
|
@ -86,7 +81,7 @@ function RSFormCard() {
|
|||
}, []);
|
||||
|
||||
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'
|
||||
required
|
||||
value={title}
|
||||
|
@ -126,9 +121,6 @@ function RSFormCard() {
|
|||
loading={processing}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<a href={fileURL} download={fileName} className='hidden' ref={fileRef}>
|
||||
<i aria-hidden="true"/>
|
||||
</a>
|
||||
<Button
|
||||
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
|
||||
disabled={!isClaimable || processing}
|
||||
|
|
|
@ -1,17 +1,79 @@
|
|||
import { useRSForm } from '../../context/RSFormContext';
|
||||
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() {
|
||||
const { schema } = useRSForm();
|
||||
interface RSFormStatsProps {
|
||||
stats: IRSFormStats
|
||||
}
|
||||
|
||||
function RSFormStats({stats}: RSFormStatsProps) {
|
||||
return (
|
||||
<Card widthClass='max-w-sm flex-grow'>
|
||||
<div className='flex justify-start'>
|
||||
<label className='font-semibold'>Всего конституент:</label>
|
||||
<span className='ml-2'>{schema!.items!.length}</span>
|
||||
</div>
|
||||
<PrettyJson data={schema || ''}/>
|
||||
<Card>
|
||||
<LabeledText id='count_all'
|
||||
label='Всего конституент '
|
||||
text={stats.count_all}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import BackendError from '../../components/BackendError';
|
|||
import ConstituentEditor from './ConstituentEditor';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
enum TabsList {
|
||||
CARD = 0,
|
||||
|
@ -21,7 +20,6 @@ enum TabsList {
|
|||
function RSFormTabs() {
|
||||
const { setActive, active, error, schema, loading } = useRSForm();
|
||||
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD);
|
||||
const search = useLocation().search;
|
||||
|
||||
const onEditCst = (cst: IConstituenta) => {
|
||||
console.log(`Set active cst: ${cst.alias}`);
|
||||
|
@ -34,22 +32,28 @@ function RSFormTabs() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
const tabQuery = new URLSearchParams(search).get('tab');
|
||||
const activeQuery = new URLSearchParams(search).get('active');
|
||||
const url = new URL(window.location.href);
|
||||
const activeQuery = url.searchParams.get('active');
|
||||
const activeCst = schema?.items?.find((cst) => cst.entityUID === Number(activeQuery)) || undefined;
|
||||
setTabIndex(Number(tabQuery) || TabsList.CARD);
|
||||
setActive(activeCst);
|
||||
}, [search, setTabIndex, setActive, schema?.items]);
|
||||
}, [setActive, schema?.items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema) {
|
||||
let url = `/rsforms/${schema.id}?tab=${tabIndex}`
|
||||
if (active) {
|
||||
url = url + `&active=${active.entityUID}`
|
||||
}
|
||||
window.history.replaceState(null, '', url);
|
||||
const url = new URL(window.location.href);
|
||||
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) {
|
||||
url.searchParams.set('active', String(active.entityUID));
|
||||
} else {
|
||||
url.searchParams.delete('active');
|
||||
}
|
||||
}, [tabIndex, active, schema]);
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
}, [tabIndex, active]);
|
||||
|
||||
return (
|
||||
<div className='container w-full'>
|
||||
|
@ -70,7 +74,7 @@ function RSFormTabs() {
|
|||
|
||||
<TabPanel className='flex items-start w-full gap-2'>
|
||||
<RSFormCard />
|
||||
<RSFormStats />
|
||||
{schema.stats && <RSFormStats stats={schema.stats}/>}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel className='w-fit'>
|
||||
|
|
Loading…
Reference in New Issue
Block a user