Improve RSForm edit UI

This commit is contained in:
IRBorisov 2023-07-18 14:55:40 +03:00
parent f26ba55fef
commit b8b8143b51
18 changed files with 392 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
function Divider() {
return (
<div
className='my-2 border-b'
/>
);
}
export default Divider;

View 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;

View File

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

View File

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

View File

@ -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
@ -152,4 +173,93 @@ export function GetCstTypeLabel(type: CstType) {
case CstType.PREDICATE: return 'Предикат-функция'; case CstType.PREDICATE: return 'Предикат-функция';
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
),
}
} }

View File

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

View File

@ -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}
@ -160,7 +152,7 @@ function RSFormCard() {
<div className='flex justify-start mt-2'> <div className='flex justify-start mt-2'>
<label className='font-semibold'>Дата создания:</label> <label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{new Date(schema!.time_create).toLocaleString(intl.locale)}</span> <span className='ml-8'>{new Date(schema!.time_create).toLocaleString(intl.locale)}</span>
</div> </div>
</form> </form>
); );
} }

View File

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

View File

@ -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');
if (active) { setTabIndex(Number(tabQuery) || TabsList.CARD);
url = url + `&active=${active.entityUID}` }, [setTabIndex]);
}
window.history.replaceState(null, '', url); 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 ( 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'>