Move to Vite. Refactor type system for data transport

This commit is contained in:
IRBorisov 2023-07-26 23:11:00 +03:00
parent a669f70efc
commit 20bea9c067
63 changed files with 1877 additions and 15783 deletions

View File

@ -10,7 +10,8 @@ function RunServer() {
RunBackend
RunFrontend
Start-Sleep -Seconds 1
Start-Process "http://127.0.0.1:8000/"
Start-Process "http://localhost:8000/"
Start-Process "http://localhost:3000/"
}
function RunBackend() {
@ -29,7 +30,8 @@ function RunBackend() {
function RunFrontend() {
Set-Location $PSScriptRoot\frontend
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'react'; & npm run start }"
& npm install
Invoke-Expression "cmd /c start powershell -Command { `$Host.UI.RawUI.WindowTitle = 'react'; & npm run dev }"
}
function FlushData {

View File

@ -1,6 +1,8 @@
# Application settings
SECRET_KEY=django-insecure-)rq@!&v7l2r%2%q#n!uq+zk@=&yc0^&ql^7%2!%9u)vt1x&j=d
ALLOWED_HOSTS=rs.acconcept.ru;localhost
CSRF_TRUSTED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000
CORS_ALLOWED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000
# File locations

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-24 19:06
# Generated by Django 4.2.3 on 2023-07-26 15:19
import apps.rsform.models
from django.conf import settings
@ -39,7 +39,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(default=-1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')),
('alias', models.CharField(default='undefined', max_length=8, verbose_name='Имя')),
('csttype', models.CharField(choices=[('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип')),
('cst_type', models.CharField(choices=[('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип')),
('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')),
('term_raw', models.TextField(blank=True, default='', verbose_name='Термин (с отсылками)')),
('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')),

View File

@ -86,11 +86,12 @@ class RSForm(models.Model):
schema=self,
order=position,
alias=alias,
csttype=type
cst_type=type
)
self._update_from_core()
self.save()
return Constituenta.objects.get(pk=result.pk)
result.refresh_from_db()
return result
@transaction.atomic
def insert_last(self, alias: str, type: CstType) -> 'Constituenta':
@ -102,7 +103,7 @@ class RSForm(models.Model):
schema=self,
order=position,
alias=alias,
csttype=type
cst_type=type
)
self._update_from_core()
self.save()
@ -217,7 +218,7 @@ class Constituenta(models.Model):
max_length=8,
default='undefined'
)
csttype = models.CharField(
cst_type = models.CharField(
verbose_name='Тип',
max_length=10,
choices=CstType.choices,
@ -274,7 +275,7 @@ class Constituenta(models.Model):
alias=data['alias'],
schema=schema,
order=order,
csttype=data['cstType'],
cst_type=data['cstType'],
convention=data.get('convention', 'Без названия')
)
if 'definition' in data:
@ -284,9 +285,9 @@ class Constituenta(models.Model):
cst.definition_raw = data['definition']['text'].get('raw', '')
cst.definition_resolved = data['definition']['text'].get('resolved', '')
if 'term' in data:
cst.term_raw = data['definition']['text'].get('raw', '')
cst.term_resolved = data['definition']['text'].get('resolved', '')
cst.term_forms = data['definition']['text'].get('forms', [])
cst.term_raw = data['term'].get('raw', '')
cst.term_resolved = data['term'].get('resolved', '')
cst.term_forms = data['term'].get('forms', [])
cst.save()
return cst
@ -294,7 +295,7 @@ class Constituenta(models.Model):
return {
'entityUID': self.id,
'type': 'constituenta',
'cstType': self.csttype,
'cstType': self.cst_type,
'alias': self.alias,
'convention': self.convention,
'term': {

View File

@ -24,7 +24,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
class Meta:
model = Constituenta
fields = '__all__'
read_only_fields = ('id', 'order', 'alias', 'csttype')
read_only_fields = ('id', 'order', 'alias', 'cst_type')
def update(self, instance: Constituenta, validated_data):
instance.schema.save()
@ -48,8 +48,8 @@ class StandaloneCstSerializer(serializers.ModelSerializer):
class CstCreateSerializer(serializers.Serializer):
alias = serializers.CharField(max_length=8)
csttype = serializers.CharField(max_length=10)
insert_after = serializers.IntegerField(required=False)
cst_type = serializers.CharField(max_length=10)
insert_after = serializers.IntegerField(required=False, allow_null=True)
class CstListSerlializer(serializers.Serializer):

View File

@ -53,7 +53,7 @@ class TestConstituenta(TestCase):
self.assertEqual(cst.schema, self.schema1)
self.assertEqual(cst.order, 1)
self.assertEqual(cst.alias, 'X1')
self.assertEqual(cst.csttype, CstType.BASE)
self.assertEqual(cst.cst_type, CstType.BASE)
self.assertEqual(cst.convention, '')
self.assertEqual(cst.definition_formal, '')
self.assertEqual(cst.term_raw, '')

View File

@ -174,14 +174,14 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(response.status_code, 403)
def test_create_constituenta(self):
data = json.dumps({'alias': 'X3', 'csttype': 'basic'})
data = json.dumps({'alias': 'X3', 'cst_type': 'basic'})
response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 403)
schema = self.rsform_owned
Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
@ -189,7 +189,7 @@ class TestRSFormViewset(APITestCase):
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 3)
data = json.dumps({'alias': 'X4', 'csttype': 'basic', 'insert_after': x2.id})
data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id})
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
@ -204,8 +204,8 @@ class TestRSFormViewset(APITestCase):
data=data, content_type='application/json')
self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [{'id': x1.id}]})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/',
data=data, content_type='application/json')
@ -217,7 +217,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x2.alias, 'X2')
self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', cst_type='basic', order=1)
data = json.dumps({'items': [{'id': x3.id}]})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-multidelete/',
data=data, content_type='application/json')
@ -230,8 +230,8 @@ class TestRSFormViewset(APITestCase):
data=data, content_type='application/json')
self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
x1 = Constituenta.objects.create(schema=schema, alias='X1', cst_type='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [{'id': x2.id}], 'move_to': 1})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/',
data=data, content_type='application/json')
@ -242,7 +242,7 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x1.order, 2)
self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', cst_type='basic', order=1)
data = json.dumps({'items': [{'id': x3.id}], 'move_to': 1})
response = self.client.patch(f'/api/rsforms/{schema.id}/cst-moveto/',
data=data, content_type='application/json')

View File

@ -56,16 +56,18 @@ class RSFormViewSet(viewsets.ModelViewSet):
schema: models.RSForm = self.get_object()
serializer = serializers.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
if ('insert_after' in serializer.validated_data):
if ('insert_after' in serializer.validated_data and serializer.validated_data['insert_after'] is not None):
cstafter = models.Constituenta.objects.get(pk=serializer.validated_data['insert_after'])
constituenta = schema.insert_at(cstafter.order + 1,
serializer.validated_data['alias'],
serializer.validated_data['csttype'])
serializer.validated_data['cst_type'])
else:
constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['csttype'])
constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['cst_type'])
schema.refresh_from_db()
outSerializer = serializers.RSFormDetailsSerlializer(schema)
response = Response(status=201, data={'new_cst': constituenta.to_json(), 'schema': outSerializer.data})
response = Response(status=201, data={
'new_cst': serializers.ConstituentaSerializer(constituenta).data,
'schema': outSerializer.data})
response['Location'] = constituenta.get_absolute_url()
return response
@ -175,13 +177,15 @@ def create_rsform(request):
data = utils.read_trs(request.FILES['file'].file)
if ('title' in request.data and request.data['title'] != ''):
data['title'] = request.data['title']
if data['title'] == '':
data['title'] = 'Без названия ' + request.FILES['file'].fileName
if ('alias' in request.data and request.data['alias'] != ''):
data['alias'] = request.data['alias']
if ('comment' in request.data and request.data['comment'] != ''):
data['comment'] = request.data['comment']
is_common = True
if ('is_common' in request.data):
is_common = request.data['is_common']
is_common = request.data['is_common'] == 'true'
schema = models.RSForm.import_json(owner, data, is_common)
result = serializers.RSFormSerializer(schema)
return Response(status=201, data=result.data)

View File

@ -64,12 +64,10 @@ REST_FRAMEWORK = {
],
}
CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
# TODO: use env setup to populate allowed_origins in production
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';')
CORS_ALLOWED_ORIGINS = os.environ.get('CORS_ALLOWED_ORIGINS', 'http://localhost:3000').split(';')
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:3000').split(';')
MIDDLEWARE = [

View File

@ -4,28 +4,27 @@
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended",
"plugin:react/jsx-runtime"
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": ["tsconfig.json"]
"project": ["tsconfig.json", "tsconfig.node.json"]
},
"plugins": [
"react", "simple-import-sort"
"react-refresh", "simple-import-sort"
],
"rules": {
"react-refresh/only-export-components": [
"off",
{ "allowConstantExport": true }
],
"simple-import-sort/imports": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"no-trailing-spaces": "warn",
"no-multiple-empty-lines": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/indent": "off",
"object-shorthand": "off"
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-argument": "off"
}
}

View File

@ -1,23 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -7,16 +7,10 @@ RUN apt-get update -qq && \
# ======= Build =======
FROM node-base as builder
ENV NODE_ENV production
WORKDIR /result
# Install dependencies
COPY *.json *.js ./
RUN npm ci --only=production
# Build deployment files
COPY ./public ./public
COPY ./src ./src
COPY ./ ./
RUN npm install
RUN npm run build
# ========= Server =======
@ -33,7 +27,7 @@ USER node
# Bring up deployment files
WORKDIR /home/node
COPY --chown=node:node --from=builder /result/build ./
COPY --chown=node:node --from=builder /result/dist ./
# Start server through docker-compose
# serve -s /home/node -l 3000

View File

@ -1,16 +1,14 @@
<!DOCTYPE html>
<html lang="ru">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Веб-приложение для работы с концептуальными схемами" />
<link rel="manifest" id="manifest-placeholder" href="%PUBLIC_URL%/manifest.json" />
<title>Концепт Портал</title>
<script>
if (
localStorage.getItem('darkMode') === 'true' ||
@ -25,7 +23,7 @@
</script>
</head>
<body>
<noscript>Включите использование JavaScript для работы с данным веб-приложением.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.34",
"@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",
@ -18,42 +18,26 @@
"react-error-boundary": "^4.0.10",
"react-intl": "^6.4.4",
"react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.12.1",
"react-scripts": "^5.0.1",
"react-router-dom": "^6.14.2",
"react-select": "^5.7.4",
"react-tabs": "^6.0.1",
"react-toastify": "^9.1.3",
"styled-components": "^6.0.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"react-tabs": "^6.0.2",
"react-toastify": "^9.1.3"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@types/node": "^20.4.5",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"tailwindcss": "^3.3.2"
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,18 +0,0 @@
{
"short_name": "КонцептПортал",
"name": "Портал для работы с концептуальными схемами",
"icons": [
{
"src": "favicon.svg",
"sizes": "64x64 32x32 24x24 16x16"
},
{
"src": "logo.svg",
"sizes": "512x512 256x256 64x64 32x32 24x24 16x16"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,4 +1,4 @@
import axios, { type AxiosError } from 'axios';
import axios, { type AxiosError,AxiosHeaderValue } from 'axios';
import PrettyJson from './Common/PrettyJSON';
@ -29,15 +29,29 @@ function DescribeError(error: ErrorInfo) {
);
}
const isHtml = error.response.headers['content-type'].includes('text/html');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const isHtml = (() => {
if (!error.response) {
return false;
}
const header = error.response.headers['content-type'] as AxiosHeaderValue;
if (!header) {
return false;
}
if (typeof header === 'number' || typeof header === 'boolean') {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return header.includes('text/html');
})();
return (
<div className='flex flex-col justify-start'>
<p className='underline'>Ошибка</p>
<p>{error.message}</p>
{error.response.data && (<>
<p className='mt-2 underline'>Описание</p>
{ isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data }} /> }
{ !isHtml && <PrettyJson data={error.response.data} />}
{ isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> }
{ !isHtml && <PrettyJson data={error.response.data as object} />}
</>)}
</div>
);

View File

@ -4,7 +4,8 @@ import { Tab } from 'react-tabs';
function ConceptTab({ children, className, ...otherProps }: TabProps) {
return (
<Tab
className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab ${className?.toString() ?? ''} whitespace-nowrap`}
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
className={`px-2 py-1 text-sm hover:cursor-pointer clr-tab ${className} whitespace-nowrap`}
{...otherProps}
>
{children}

View File

@ -1,7 +1,7 @@
interface LabeledTextProps {
id?: string
label: string
text: any
text: string | number
tooltip?: string
}

View File

@ -1,5 +1,5 @@
interface PrettyJsonProps {
data: any
data: unknown
}
function PrettyJson({ data }: PrettyJsonProps) {

View File

@ -9,7 +9,7 @@ interface TextAreaProps {
placeholder?: string
widthClass?: string
rows?: number
value?: any
value?: string | ReadonlyArray<string> | number | undefined;
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
onFocus?: () => void
}

View File

@ -1,8 +1,12 @@
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptTheme } from '../context/ThemeContext';
function ToasterThemed({ theme, ...props }: ToastContainerProps) {
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'>{}
function ToasterThemed({ ...props }: ToasterThemedProps) {
const { darkMode } = useConceptTheme();
return (
@ -12,4 +16,5 @@ function ToasterThemed({ theme, ...props }: ToastContainerProps) {
/>
);
}
export default ToasterThemed;

View File

@ -2,14 +2,14 @@ import { createContext, useCallback, useContext, useLayoutEffect, useState } fro
import { type ErrorInfo } from '../components/BackendError';
import useLocalStorage from '../hooks/useLocalStorage';
import { type BackendCallback, getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
import { type ICurrentUser, type IUserSignupData } from '../utils/models';
import { type DataCallback, getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI';
import { ICurrentUser, IUserLoginData, IUserProfile, IUserSignupData } from '../utils/models';
interface IAuthContext {
user: ICurrentUser | undefined
login: (username: string, password: string, callback?: BackendCallback) => void
logout: (callback?: BackendCallback) => void
signup: (data: IUserSignupData, callback?: BackendCallback) => void
login: (data: IUserLoginData, callback?: DataCallback) => void
logout: (callback?: DataCallback) => void
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
loading: boolean
error: ErrorInfo
setError: (error: ErrorInfo) => void
@ -35,69 +35,55 @@ export const AuthState = ({ children }: AuthStateProps) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const loadCurrentUser = useCallback(
async () => {
await getAuth({
const reload = useCallback(
(callback?: () => void) => {
getAuth({
onError: () => { setUser(undefined); },
onSuccess: response => {
if (response.data.id) {
setUser(response.data);
onSuccess: currentUser => {
if (currentUser.id) {
setUser(currentUser);
} else {
setUser(undefined)
setUser(undefined);
}
if (callback) callback();
}
});
}, [setUser]
);
function login(uname: string, pw: string, callback?: BackendCallback) {
function login(data: IUserLoginData, callback?: DataCallback) {
setError(undefined);
postLogin({
data: { username: uname, password: pw },
data: data,
showError: true,
setLoading,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess:
(response) => {
loadCurrentUser()
.then(() => { if (callback) callback(response); })
.catch(console.error);
}
}).catch(console.error);
onSuccess: newData => reload(() => { if (callback) callback(newData); })
});
}
function logout(callback?: BackendCallback) {
function logout(callback?: DataCallback) {
setError(undefined);
postLogout({
showError: true,
onSuccess:
(response) => {
loadCurrentUser()
.then(() => { if (callback) callback(response); })
.catch(console.error);
}
}).catch(console.error);
onSuccess: newData => reload(() => { if (callback) callback(newData); })
});
}
function signup(data: IUserSignupData, callback?: BackendCallback) {
function signup(data: IUserSignupData, callback?: DataCallback<IUserProfile>) {
setError(undefined);
postSignup({
data,
data: data,
showError: true,
setLoading,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess:
(response) => {
loadCurrentUser()
.then(() => { if (callback) callback(response); })
.catch(console.error);
}
}).catch(console.error);
onSuccess: newData => reload(() => { if (callback) callback(newData); })
});
}
useLayoutEffect(() => {
loadCurrentUser().catch(console.error);
}, [loadCurrentUser])
reload();
}, [reload])
return (
<AuthContext.Provider

View File

@ -4,11 +4,14 @@ import { toast } from 'react-toastify'
import { type ErrorInfo } from '../components/BackendError'
import { useRSFormDetails } from '../hooks/useRSFormDetails'
import {
type BackendCallback, deleteRSForm, getTRSFile,
type DataCallback, deleteRSForm, getTRSFile,
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchRSForm,
postClaimRSForm, postNewConstituenta
} from '../utils/backendAPI'
import { type IConstituenta, type IRSForm } from '../utils/models'
import {
IConstituenta, IConstituentaList, IConstituentaMeta, ICstCreateData,
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormMeta, IRSFormUpdateData
} from '../utils/models'
import { useAuth } from './AuthContext'
interface IRSFormContext {
@ -32,15 +35,15 @@ interface IRSFormContext {
toggleReadonly: () => void
toggleTracking: () => void
update: (data: any, callback?: BackendCallback) => void
destroy: (callback?: BackendCallback) => void
claim: (callback?: BackendCallback) => void
download: (callback: BackendCallback) => void
update: (data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => void
destroy: (callback?: DataCallback) => void
claim: (callback?: DataCallback<IRSFormMeta>) => void
download: (callback: DataCallback<Blob>) => void
cstUpdate: (cstdID: string, data: any, callback?: BackendCallback) => void
cstCreate: (data: any, callback?: BackendCallback) => void
cstDelete: (data: any, callback?: BackendCallback) => void
cstMoveTo: (data: any, callback?: BackendCallback) => void
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void
cstDelete: (data: IConstituentaList, callback?: () => void) => void
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void
}
const RSFormContext = createContext<IRSFormContext | null>(null)
@ -76,7 +79,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
!loading && !isReadonly &&
((isOwned || (isForceAdmin && user?.is_staff)) ?? false)
)
}, [user, isReadonly, isForceAdmin, isOwned, loading])
}, [user?.is_staff, isReadonly, isForceAdmin, isOwned, loading])
const activeCst = useMemo(
() => {
@ -94,34 +97,39 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
}, [])
const update = useCallback(
(data: any, callback?: BackendCallback) => {
(data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => {
if (!schema) {
return;
}
setError(undefined)
patchRSForm(schemaID, {
data,
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
reload(setProcessing)
.then(() => { if (callback != null) callback(response); })
.catch(console.error);
onSuccess: newData => {
setSchema(Object.assign(schema, newData));
if (callback) callback(newData);
}
}).catch(console.error);
}, [schemaID, setError, reload])
});
}, [schemaID, setError, setSchema, schema])
const destroy = useCallback(
(callback?: BackendCallback) => {
(callback?: DataCallback) => {
setError(undefined)
deleteRSForm(schemaID, {
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: callback
}).catch(console.error);
}, [schemaID, setError])
onSuccess: newData => {
setSchema(undefined);
if (callback) callback(newData);
}
});
}, [schemaID, setError, setSchema])
const claim = useCallback(
(callback?: BackendCallback) => {
(callback?: DataCallback<IRSFormMeta>) => {
if (!schema || !user) {
return;
}
@ -130,85 +138,81 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
schema.owner = user.id;
schema.time_update = response.data.time_update;
setSchema(schema);
if (callback != null) callback(response);
onSuccess: newData => {
setSchema(Object.assign(schema, newData));
if (callback) callback(newData);
}
}).catch(console.error);
});
}, [schemaID, setError, schema, user, setSchema])
const download = useCallback(
(callback: BackendCallback) => {
(callback: DataCallback<Blob>) => {
setError(undefined)
getTRSFile(schemaID, {
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: callback
}).catch(console.error);
});
}, [schemaID, setError])
const cstUpdate = useCallback(
(cstID: string, data: any, callback?: BackendCallback) => {
setError(undefined)
patchConstituenta(cstID, {
data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
reload(setProcessing)
.then(() => { if (callback != null) callback(response); })
.catch(console.error);
}
}).catch(console.error);
}, [setError])
const cstCreate = useCallback(
(data: any, callback?: BackendCallback) => {
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
setError(undefined)
postNewConstituenta(schemaID, {
data,
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
setSchema(response.data.schema);
if (callback != null) callback(response);
onSuccess: newData => {
setSchema(newData.schema);
if (callback) callback(newData.new_cst);
}
}).catch(console.error);
});
}, [schemaID, setError, setSchema]);
const cstDelete = useCallback(
(data: any, callback?: BackendCallback) => {
(data: IConstituentaList, callback?: () => void) => {
setError(undefined)
patchDeleteConstituenta(schemaID, {
data,
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
setSchema(response.data)
if (callback != null) callback(response)
onSuccess: newData => {
setSchema(newData);
if (callback) callback();
}
}).catch(console.error);
});
}, [schemaID, setError, setSchema]);
const cstMoveTo = useCallback(
(data: any, callback?: BackendCallback) => {
const cstUpdate = useCallback(
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
setError(undefined)
patchMoveConstituenta(schemaID, {
data,
patchConstituenta(String(data.id), {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: (response) => {
setSchema(response.data);
if (callback != null) callback(response);
onSuccess: newData => {
reload(setProcessing, () => { if (callback != null) callback(newData); })
}
}).catch(console.error);
});
}, [setError, reload])
const cstMoveTo = useCallback(
(data: ICstMovetoData, callback?: () => void) => {
setError(undefined)
patchMoveConstituenta(schemaID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSuccess: newData => {
setSchema(newData);
if (callback) callback();
}
});
}, [schemaID, setError, setSchema]);
return (

View File

@ -5,8 +5,8 @@ import { type IUserInfo } from '../utils/models';
interface IUsersContext {
users: IUserInfo[]
reload: () => Promise<void>
getUserLabel: (userID?: number) => string
reload: () => void
getUserLabel: (userID: number | null) => string
}
const UsersContext = createContext<IUsersContext | null>(null)
@ -27,13 +27,13 @@ interface UsersStateProps {
export const UsersState = ({ children }: UsersStateProps) => {
const [users, setUsers] = useState<IUserInfo[]>([])
const getUserLabel = (userID?: number) => {
const getUserLabel = (userID: number | null) => {
const user = users.find(({ id }) => id === userID)
if (user == null) {
if (!user) {
return (userID ? userID.toString() : 'Отсутствует');
}
const hasFirstName = user.first_name != null && user.first_name !== '';
const hasLastName = user.last_name != null && user.last_name !== '';
const hasFirstName = user.first_name !== '';
const hasLastName = user.last_name !== '';
if (hasFirstName || hasLastName) {
if (!hasLastName) {
return user.first_name;
@ -47,17 +47,17 @@ export const UsersState = ({ children }: UsersStateProps) => {
}
const reload = useCallback(
async () => {
await getActiveUsers({
() => {
getActiveUsers({
showError: true,
onError: () => { setUsers([]); },
onSuccess: response => { setUsers(response.data); }
onSuccess: newData => { setUsers(newData); }
});
}, [setUsers]
)
useEffect(() => {
reload().catch(console.error);
reload();
}, [reload])
return (

View File

@ -1,28 +1,27 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useState } from 'react'
import { type ErrorInfo } from '../components/BackendError';
import { postCheckExpression } from '../utils/backendAPI';
import { type IRSForm } from '../utils/models';
import { DataCallback, postCheckExpression } from '../utils/backendAPI';
import { ExpressionParse, type IRSForm } from '../utils/models';
function useCheckExpression({ schema }: { schema?: IRSForm }) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const [parseData, setParseData] = useState<any | undefined>(undefined);
const [parseData, setParseData] = useState<ExpressionParse | undefined>(undefined);
const resetParse = useCallback(() => { setParseData(undefined); }, []);
async function checkExpression(expression: string, onSuccess?: (response: AxiosResponse) => void) {
function checkExpression(expression: string, onSuccess?: DataCallback<ExpressionParse>) {
setError(undefined);
setParseData(undefined);
await postCheckExpression(String(schema?.id), {
data: { expression },
postCheckExpression(String(schema?.id), {
data: { expression: expression },
showError: true,
setLoading,
onError: error => { setError(error); },
onSuccess: (response) => {
setParseData(response.data);
if (onSuccess) onSuccess(response);
onSuccess: newData => {
setParseData(newData);
if (onSuccess) onSuccess(newData);
}
});
}

View File

@ -15,6 +15,6 @@ function useClickedOutside({ ref, callback }: { ref: React.RefObject<HTMLElement
document.removeEventListener('mouseup', handleClickOutside);
};
}, [ref, callback]);
};
}
export default useClickedOutside;

View File

@ -15,6 +15,6 @@ function useDropdown() {
toggle: () => { setIsActive(!isActive); },
hide: () => { setIsActive(false); }
};
};
}
export default useDropdown;

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
function getStorageValue<ValueType>(key: string, defaultValue: ValueType) {
const saved = localStorage.getItem(key);
const initial = saved ? JSON.parse(saved) : undefined;
const initial = saved ? JSON.parse(saved) as ValueType : undefined;
return initial || defaultValue;
}

View File

@ -1,28 +1,21 @@
import { useState } from 'react'
import { type ErrorInfo } from '../components/BackendError';
import { postNewRSForm } from '../utils/backendAPI';
import { DataCallback, postNewRSForm } from '../utils/backendAPI';
import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
function useNewRSForm() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
async function createSchema({ data, file, onSuccess }: {
data: any
file?: File
onSuccess: (newID: string) => void
}) {
function createSchema(data: IRSFormCreateData, onSuccess: DataCallback<IRSFormMeta>) {
setError(undefined);
if (file) {
data.file = file;
data.fileName = file.name;
}
await postNewRSForm({
data,
postNewRSForm({
data: data,
showError: true,
setLoading,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: response => { onSuccess(response.data.id); }
onSuccess: onSuccess
});
}

View File

@ -2,40 +2,43 @@ import { useCallback, useEffect, useState } from 'react'
import { type ErrorInfo } from '../components/BackendError';
import { getRSFormDetails } from '../utils/backendAPI';
import { CalculateStats, type IRSForm } from '../utils/models'
import { IRSForm, IRSFormData,LoadRSFormData } from '../utils/models'
export function useRSFormDetails({ target }: { target?: string }) {
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
function setSchema(schema?: IRSForm) {
if (schema) CalculateStats(schema);
function setSchema(data?: IRSFormData) {
if (!data) {
setInnerSchema(undefined);
return;
}
const schema = LoadRSFormData(data);
setInnerSchema(schema);
console.log('Loaded schema: ', schema);
}
const fetchData = useCallback(
async (setCustomLoading?: typeof setLoading) => {
const reload = useCallback(
(setCustomLoading?: typeof setLoading, callback?: () => void) => {
setError(undefined);
if (!target) {
return;
}
await getRSFormDetails(target, {
getRSFormDetails(target, {
showError: true,
setLoading: setCustomLoading ?? setLoading,
onError: error => { setInnerSchema(undefined); setError(error); },
onSuccess: (response) => { setSchema(response.data); }
onSuccess: schema => {
setSchema(schema);
if (callback) callback();
}
});
}, [target]);
async function reload(setCustomLoading?: typeof setLoading) {
await fetchData(setCustomLoading);
}
useEffect(() => {
fetchData().catch((error) => { setError(error); });
}, [fetchData])
reload();
}, [reload])
return { schema, setSchema, reload, error, setError, loading };
}

View File

@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
import { type ErrorInfo } from '../components/BackendError';
import { getRSForms } from '../utils/backendAPI';
import { type IRSForm } from '../utils/models'
import { IRSFormMeta } from '../utils/models'
export enum FilterType {
PERSONAL = 'personal',
@ -11,20 +11,20 @@ export enum FilterType {
export interface RSFormsFilter {
type: FilterType
data?: any
data?: number | null
}
export function useRSForms() {
const [rsforms, setRSForms] = useState<IRSForm[]>([]);
const [rsforms, setRSForms] = useState<IRSFormMeta[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const loadList = useCallback(async (filter: RSFormsFilter) => {
await getRSForms(filter, {
const loadList = useCallback((filter: RSFormsFilter) => {
getRSForms(filter, {
showError: true,
setLoading,
onError: error => { setError(error); },
onSuccess: response => { setRSForms(response.data); }
onSuccess: newData => { setRSForms(newData); }
});
}, []);

View File

@ -10,21 +10,20 @@ export function useUserProfile() {
const [error, setError] = useState<ErrorInfo>(undefined);
const fetchUser = useCallback(
async () => {
() => {
setError(undefined);
setUser(undefined);
await getProfile({
getProfile({
showError: true,
setLoading,
setLoading: setLoading,
onError: error => { setError(error); },
onSuccess: response => { setUser(response.data); }
onSuccess: newData => { setUser(newData); }
});
}, [setUser]
)
useEffect(() => {
fetchUser().catch((error) => { setError(error); });
fetchUser();
}, [fetchUser])
return { user, fetchUser, error, loading };

View File

@ -1,28 +1,21 @@
'use client';
import './index.css';
import 'react-toastify/dist/ReactToastify.css';
import './index.css'
import axios from 'axios';
import React from 'react';
import ReactDOM from 'react-dom/client';
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import ErrorFallback from './components/ErrorFallback';
import { AuthState } from './context/AuthContext';
import { ThemeState } from './context/ThemeContext';
import { UsersState } from './context/UsersContext';
import App from './App.tsx'
import ErrorFallback from './components/ErrorFallback.tsx';
import { AuthState } from './context/AuthContext.tsx';
import { ThemeState } from './context/ThemeContext.tsx';
import { UsersState } from './context/UsersContext.tsx';
axios.defaults.withCredentials = true
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'x-csrftoken'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
axios.defaults.withCredentials = true;
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'x-csrftoken';
const resetState = () => {
console.log('Resetting state after error fallback')
@ -33,7 +26,7 @@ const logError = (error: Error, info: { componentStack: string }) => {
console.log('Component stack: ' + info.componentStack)
};
root.render(
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<ErrorBoundary
@ -52,5 +45,5 @@ root.render(
</IntlProvider>
</ErrorBoundary>
</BrowserRouter>
</React.StrictMode>
);
</React.StrictMode>,
)

View File

@ -8,6 +8,7 @@ import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext';
import { IUserLoginData } from '../utils/models';
function LoginPage() {
const [username, setUsername] = useState('');
@ -30,7 +31,11 @@ function LoginPage() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!loading) {
login(username, password, () => { navigate('/rsforms?filter=personal'); });
const data: IUserLoginData = {
username: username,
password: password
};
login(data, () => { navigate('/rsforms?filter=personal'); });
}
};

View File

@ -11,7 +11,7 @@ import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput';
import RequireAuth from '../components/RequireAuth';
import useNewRSForm from '../hooks/useNewRSForm';
import { type IRSFormCreateData } from '../utils/models';
import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
function RSFormCreatePage() {
const navigate = useNavigate();
@ -30,9 +30,9 @@ function RSFormCreatePage() {
}
}
const onSuccess = (newID: string) => {
const onSuccess = (newSchema: IRSFormMeta) => {
toast.success('Схема успешно создана');
navigate(`/rsforms/${newID}`);
navigate(`/rsforms/${newSchema.id}`);
}
const { createSchema, error, setError, loading } = useNewRSForm()
@ -46,16 +46,14 @@ function RSFormCreatePage() {
return;
}
const data: IRSFormCreateData = {
title,
alias,
comment,
is_common: common
title: title,
alias: alias,
comment: comment,
is_common: common,
file: file,
fileName: file?.name
};
void createSchema({
data,
file,
onSuccess
});
void createSchema(data, onSuccess);
};
return (

View File

@ -1,18 +1,20 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea';
import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { type CstType, EditMode, type INewCstData } from '../../utils/models';
import { type CstType, EditMode, type ICstCreateData, ICstUpdateData } from '../../utils/models';
import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
import ConstituentsSideList from './ConstituentsSideList';
import CreateCstModal from './CreateCstModal';
import ExpressionEditor from './ExpressionEditor';
import { RSFormTabsList } from './RSFormTabs';
function ConstituentEditor() {
const navigate = useNavigate();
const {
activeCst, activeID, schema, setActiveID, processing, isEditable,
cstDelete, cstUpdate, cstCreate
@ -33,10 +35,8 @@ function ConstituentEditor() {
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
useLayoutEffect(() => {
if (schema?.items && schema?.items.length > 0) {
// TODO: figure out why schema.items could be undef?
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setActiveID((prev) => (prev ?? schema?.items![0].id ?? undefined));
if (schema && schema?.items.length > 0) {
setActiveID((prev) => (prev ?? schema.items[0].id ?? undefined));
}
}, [schema, setActiveID]);
@ -46,12 +46,14 @@ function ConstituentEditor() {
return;
}
setIsModified(
activeCst.term?.raw !== term ||
activeCst.definition?.text?.raw !== textDefinition ||
activeCst.term.raw !== term ||
activeCst.definition.text.raw !== textDefinition ||
activeCst.convention !== convention ||
activeCst.definition?.formal !== expression
activeCst.definition.formal !== expression
);
}, [activeCst, term, textDefinition, expression, convention]);
}, [activeCst, activeCst?.term, activeCst?.definition.formal,
activeCst?.definition.text.raw, activeCst?.convention,
term, textDefinition, expression, convention]);
useLayoutEffect(() => {
if (activeCst) {
@ -61,25 +63,25 @@ function ConstituentEditor() {
setTerm(activeCst.term?.raw ?? '');
setTextDefinition(activeCst.definition?.text?.raw ?? '');
setExpression(activeCst.definition?.formal ?? '');
setTypification(activeCst?.parse?.typification ?? 'N/A');
setTypification(activeCst?.parse?.typification || 'N/A');
}
}, [activeCst]);
const handleSubmit =
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!processing) {
const data = {
alias: alias,
convention: convention,
definition_formal: expression,
definition_raw: textDefinition,
term_raw: term
};
cstUpdate(String(activeID), data, () => {
toast.success('Изменения сохранены');
});
if (!activeID || processing) {
return;
}
const data: ICstUpdateData = {
id: activeID,
alias: alias,
convention: convention,
definition_formal: expression,
definition_raw: textDefinition,
term_raw: term
};
cstUpdate(data, () => { toast.success('Изменения сохранены'); });
};
const handleDelete = useCallback(
@ -98,25 +100,24 @@ function ConstituentEditor() {
}, [activeID, schema, setActiveID, cstDelete]);
const handleAddNew = useCallback(
(csttype?: CstType) => {
(type?: CstType) => {
if (!activeID || !schema?.items) {
return;
}
if (!csttype) {
if (!type) {
setShowCstModal(true);
} else {
const data: INewCstData = {
csttype: csttype,
alias: createAliasFor(csttype, schema),
const data: ICstCreateData = {
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: activeID
}
cstCreate(data,
(response: AxiosResponse) => {
setActiveID(response.data.new_cst.id);
toast.success(`Конституента добавлена: ${response.data.new_cst.alias as string}`);
cstCreate(data, newCst => {
navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${newCst.id}`);
toast.success(`Конституента добавлена: ${newCst.alias}`);
});
}
}, [activeID, schema, cstCreate, setActiveID]);
}, [activeID, schema, cstCreate, navigate]);
const handleRename = useCallback(() => {
toast.info('Переименование в разработке');
@ -202,7 +203,7 @@ function ConstituentEditor() {
placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression}
disabled={!isEnabled}
isActive={editMode === 'rslang'}
isActive={editMode === EditMode.RSLANG}
toggleEditMode={() => { setEditMode(EditMode.RSLANG); }}
onChange={event => { setExpression(event.target.value); }}
setValue={setExpression}

View File

@ -5,7 +5,7 @@ import DataTableThemed from '../../components/Common/DataTableThemed';
import { useRSForm } from '../../context/RSFormContext';
import useLocalStorage from '../../hooks/useLocalStorage';
import { CstType, type IConstituenta, matchConstituenta } from '../../utils/models';
import { extractGlobals } from '../../utils/staticUI';
import { extractGlobals, getMockConstituenta } from '../../utils/staticUI';
interface ConstituentsSideListProps {
expression: string
@ -20,19 +20,16 @@ function ConstituentsSideList({ expression }: ConstituentsSideListProps) {
useEffect(() => {
if (!schema?.items) {
setFilteredData([]);
} else if (onlyExpression) {
return;
}
if (onlyExpression) {
const aliases = extractGlobals(expression);
const filtered = schema?.items.filter((cst) => aliases.has(cst.alias));
const names = filtered.map(cst => cst.alias)
const diff = Array.from(aliases).filter(name => !names.includes(name));
if (diff.length > 0) {
diff.forEach(
(alias, i) => filtered.push({
id: -i,
alias: alias,
convention: 'Конституента отсутствует',
cstType: CstType.BASE
}));
(alias, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
}
setFilteredData(filtered);
} else if (!filterText) {
@ -50,7 +47,7 @@ function ConstituentsSideList({ expression }: ConstituentsSideListProps) {
}, [setActiveID]);
const handleDoubleClick = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
(cst: IConstituenta) => {
if (cst.id > 0) setActiveID(cst.id);
}, [setActiveID]);

View File

@ -1,4 +1,3 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
@ -8,8 +7,8 @@ import Divider from '../../components/Common/Divider';
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { CstType, type IConstituenta, type INewCstData, inferStatus, ParsingStatus, ValueClass } from '../../utils/models'
import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
import { CstType, type IConstituenta, type ICstCreateData, ICstMovetoData,inferStatus, ParsingStatus, ValueClass } from '../../utils/models'
import { createAliasFor, getCstTypePrefix, getCstTypeShortcut, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
import CreateCstModal from './CreateCstModal';
interface ConstituentsTableProps {
@ -51,8 +50,8 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
const data = {
items: selected.map(id => { return { id }; })
}
const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias);
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames.toString()}`));
const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias).join(', ');
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames}`));
}, [selected, schema?.items, cstDelete]);
// Move selected cst up
@ -69,10 +68,10 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
}
return Math.min(prev, index);
}, -1);
const insertIndex = Math.max(0, currentIndex - 1) + 1
const target = Math.max(0, currentIndex - 1) + 1
const data = {
items: selected.map(id => { return { id }; }),
move_to: insertIndex
move_to: target
}
cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]);
@ -95,10 +94,10 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
return Math.max(prev, index);
}
}, -1);
const insertIndex = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data = {
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data: ICstMovetoData = {
items: selected.map(id => { return { id }; }),
move_to: insertIndex
move_to: target
}
cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]);
@ -109,38 +108,56 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
}, []);
// Add new constituent
const handleAddNew = useCallback((csttype?: CstType) => {
const handleAddNew = useCallback((type?: CstType) => {
if (!schema) {
return;
}
if (!csttype) {
if (!type) {
setShowCstModal(true);
} else {
const data: INewCstData = {
csttype,
alias: createAliasFor(csttype, schema)
const selectedPosition = selected.reduce((prev, cstID) => {
const position = schema.items.findIndex(cst => cst.id === cstID);
return Math.max(position, prev);
}, -1) + 1;
const data: ICstCreateData = {
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: selectedPosition > 0 ? selectedPosition : null
}
if (selected.length > 0) {
data.insert_after = selected[selected.length - 1]
}
cstCreate(data, (response: AxiosResponse) =>
toast.success(`Добавлена конституента ${response.data.new_cst.alias as string}`));
cstCreate(data, new_cst => toast.success(`Добавлена конституента ${new_cst.alias}`));
}
}, [schema, selected, cstCreate]);
// Implement hotkeys for working with constituents table
const handleTableKey = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (!event.altKey) {
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
if (!event.altKey || !isEditable || event.shiftKey) {
return;
}
if (!isEditable || selected.length === 0) {
if (processAltKey(event.key)) {
event.preventDefault();
return;
}
switch (event.key) {
case 'ArrowUp': handleMoveUp(); return;
case 'ArrowDown': handleMoveDown();
}
function processAltKey(key: string): boolean {
if (selected.length > 0) {
switch (key) {
case 'ArrowUp': handleMoveUp(); return true;
case 'ArrowDown': handleMoveDown(); return true;
}
}
}, [isEditable, selected, handleMoveUp, handleMoveDown]);
switch (key) {
case '1': handleAddNew(CstType.BASE); return true;
case '2': handleAddNew(CstType.STRUCTURED); return true;
case '3': handleAddNew(CstType.TERM); return true;
case '4': handleAddNew(CstType.AXIOM); return true;
case 'q': handleAddNew(CstType.FUNCTION); return true;
case 'w': handleAddNew(CstType.PREDICATE); return true;
case '5': handleAddNew(CstType.CONSTANT); return true;
case '6': handleAddNew(CstType.THEOREM); return true;
}
return false;
}
const columns = useMemo(() =>
[
@ -311,7 +328,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
const type = typeStr as CstType;
return <Button key={type}
text={`${getCstTypePrefix(type)}`}
tooltip={getCstTypeLabel(type)}
tooltip={getCstTypeShortcut(type)}
dense
onClick={() => { handleAddNew(type); }}
/>;

View File

@ -1,4 +1,3 @@
import { type AxiosResponse } from 'axios';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
@ -7,7 +6,8 @@ import Label from '../../components/Common/Label';
import { Loader } from '../../components/Common/Loader';
import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression';
import { CstType, TokenID } from '../../utils/models';
import { TokenID } from '../../utils/enums';
import { CstType } from '../../utils/models';
import ParsingResult from './ParsingResult';
import RSLocalButton from './RSLocalButton';
import RSTokenButton from './RSTokenButton';
@ -47,12 +47,16 @@ function ExpressionEditor({
}
const prefix = activeCst?.alias + (activeCst?.cstType === CstType.STRUCTURED ? '::=' : ':==');
const expression = prefix + value;
checkExpression(expression, (response: AxiosResponse) => {
checkExpression(expression, parse => {
// TODO: update cursor position
if (!parse.parseResult && parse.errors.length > 0) {
const errorPosition = parse.errors[0].position - prefix.length
expressionCtrl.current!.selectionStart = errorPosition;
expressionCtrl.current!.selectionEnd = errorPosition;
}
setIsModified(false);
setTypification(response.data.typification);
toast.success('проверка завершена');
}).catch(console.error);
setTypification(parse.typification);
});
}, [value, checkExpression, activeCst, setTypification]);
const handleEdit = useCallback((id: TokenID, key?: string) => {

View File

@ -1,7 +1,8 @@
import PrettyJson from '../../components/Common/PrettyJSON';
import { ExpressionParse } from '../../utils/models';
interface ParsingResultProps {
data?: any
data?: ExpressionParse
}
function ParsingResult({ data }: ParsingResultProps) {

View File

@ -12,6 +12,7 @@ import { CrownIcon, DownloadIcon, DumpBinIcon, SaveIcon, ShareIcon } from '../..
import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext';
import { useUsers } from '../../context/UsersContext';
import { IRSFormCreateData } from '../../utils/models';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
function RSFormCard() {
@ -42,7 +43,8 @@ function RSFormCard() {
schema.comment !== comment ||
schema.is_common !== common
);
}, [schema, title, alias, comment, common]);
}, [schema, schema?.title, schema?.alias, schema?.comment, schema?.is_common,
title, alias, comment, common]);
useLayoutEffect(() => {
if (schema) {
@ -55,13 +57,12 @@ function RSFormCard() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = {
title,
alias,
comment,
const data: IRSFormCreateData = {
title: title,
alias: alias,
comment: comment,
is_common: common
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
update(data, () => toast.success('Изменения сохранены'));
};
@ -70,7 +71,6 @@ function RSFormCard() {
const handleDownload = useCallback(() => {
const fileName = (schema?.alias ?? 'Schema') + '.trs';
// eslint-disable-next-line @typescript-eslint/no-floating-promises
downloadRSFormProc(download, fileName);
}, [download, schema?.alias]);
@ -139,7 +139,7 @@ function RSFormCard() {
<div className='flex justify-start mt-2'>
<label className='font-semibold'>Владелец:</label>
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
{getUserLabel(schema?.owner)}
{getUserLabel(schema?.owner ?? null)}
</span>
</div>
<div className='flex justify-start mt-2'>

View File

@ -1,4 +1,4 @@
import { TokenID } from '../../utils/models'
import { TokenID } from '../../utils/enums';
interface RSLocalButtonProps {
text: string

View File

@ -1,4 +1,4 @@
import { type TokenID } from '../../utils/models'
import { TokenID } from '../../utils/enums';
import { getRSButtonData } from '../../utils/staticUI'
interface RSTokenButtonProps {

View File

@ -1,11 +1,11 @@
import { useMemo } from 'react';
import { ExpressionStatus, type IConstituenta, inferStatus, ParsingStatus } from '../../utils/models';
import { ExpressionParse,ExpressionStatus, type IConstituenta, inferStatus, ParsingStatus } from '../../utils/models';
import { getStatusInfo } from '../../utils/staticUI';
interface StatusBarProps {
isModified?: boolean
parseData?: any
parseData?: ExpressionParse
constituenta?: IConstituenta
}

View File

@ -1,6 +1,6 @@
// Formatted text editing helpers
import { TokenID } from '../../utils/models'
import { TokenID } from '../../utils/enums';
export function getSymbolSubstitute(input: string): string | undefined {
switch (input) {
@ -125,7 +125,7 @@ export class TextWrapper implements IManagedText {
return true;
}
case TokenID.BOOLEAN: {
if (this.selEnd !== this.selStart && this.value.at(this.selStart) === '') {
if (this.selEnd !== this.selStart && this.value[this.selStart] === '') {
this.envelopeWith('', '');
} else {
this.envelopeWith('(', ')');
@ -209,4 +209,4 @@ export class TextWrapper implements IManagedText {
}
return false;
}
};
}

View File

@ -4,10 +4,10 @@ import { useNavigate } from 'react-router-dom';
import DataTableThemed from '../../components/Common/DataTableThemed';
import { useUsers } from '../../context/UsersContext';
import { type IRSForm } from '../../utils/models'
import { IRSFormMeta } from '../../utils/models'
interface RSFormsTableProps {
schemas: IRSForm[]
schemas: IRSFormMeta[]
}
function RSFormsTable({ schemas }: RSFormsTableProps) {
@ -15,7 +15,7 @@ function RSFormsTable({ schemas }: RSFormsTableProps) {
const intl = useIntl();
const { getUserLabel } = useUsers();
const openRSForm = (schema: IRSForm, event: React.MouseEvent<Element, MouseEvent>) => {
const openRSForm = (schema: IRSFormMeta) => {
navigate(`/rsforms/${schema.id}`);
};
@ -25,7 +25,7 @@ function RSFormsTable({ schemas }: RSFormsTableProps) {
name: 'Шифр',
id: 'alias',
maxWidth: '140px',
selector: (schema: IRSForm) => schema.alias,
selector: (schema: IRSFormMeta) => schema.alias,
sortable: true,
reorder: true
},
@ -33,15 +33,15 @@ function RSFormsTable({ schemas }: RSFormsTableProps) {
name: 'Название',
id: 'title',
minWidth: '50%',
selector: (schema: IRSForm) => schema.title,
selector: (schema: IRSFormMeta) => schema.title,
sortable: true,
reorder: true
},
{
name: 'Владелец',
id: 'owner',
selector: (schema: IRSForm) => schema.owner ?? 0,
format: (schema: IRSForm) => {
selector: (schema: IRSFormMeta) => schema.owner ?? 0,
format: (schema: IRSFormMeta) => {
return getUserLabel(schema.owner);
},
sortable: true,
@ -50,8 +50,8 @@ function RSFormsTable({ schemas }: RSFormsTableProps) {
{
name: 'Обновлена',
id: 'time_update',
selector: (row: IRSForm) => row.time_update,
format: (row: IRSForm) => new Date(row.time_update).toLocaleString(intl.locale),
selector: (schema: IRSFormMeta) => schema.time_update,
format: (schema: IRSFormMeta) => new Date(schema.time_update).toLocaleString(intl.locale),
sortable: true,
reorder: true
}
@ -70,7 +70,7 @@ function RSFormsTable({ schemas }: RSFormsTableProps) {
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
<p>Список схем пуст</p>
<p>Измените фильтр</p>
<p>Измените фильтр или создайти новую концептуальную схему</p>
</span>}
pagination

View File

@ -19,7 +19,7 @@ function RSFormsPage() {
if (type === FilterType.PERSONAL) {
filter.data = user?.id;
}
loadList(filter).catch(console.error);
loadList(filter);
}, [search, user, loadList]);
return (

View File

@ -1,15 +1,17 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import BackendError from '../components/BackendError';
import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext';
import { type IUserSignupData } from '../utils/models';
function RegisterPage() {
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@ -17,7 +19,6 @@ function RegisterPage() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [success, setSuccess] = useState(false);
const { user, signup, loading, error, setError } = useAuth()
useEffect(() => {
@ -35,20 +36,18 @@ function RegisterPage() {
first_name: firstName,
last_name: lastName
};
signup(data, () => { setSuccess(true); });
signup(data, createdUser => {
navigate(`/login?username=${createdUser.username}`);
toast.success(`Пользователь успешно создан: ${createdUser.username}`);
});
}
};
return (
<div className='w-full py-2'>
{ success &&
<div className='flex flex-col items-center'>
<InfoMessage message={`Вы успешно зарегистрировали пользователя ${username}`}/>
<TextURL text='Войти в аккаунт' href={`/login?username=${username}`}/>
</div>}
{ !success && user &&
{ user &&
<InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> }
{ !success && !user &&
{ !user &&
<Form title='Регистрация пользователя' onSubmit={handleSubmit}>
<TextInput id='username' label='Имя пользователя' type='text'
required

View File

@ -1 +0,0 @@
/// <reference types='react-scripts' />

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1,253 +1,272 @@
import axios, { type AxiosResponse } from 'axios'
import axios, { AxiosRequestConfig } from 'axios'
import { toast } from 'react-toastify'
import { type ErrorInfo } from '../components/BackendError'
import { FilterType, type RSFormsFilter } from '../hooks/useRSForms'
import { FilterType, RSFormsFilter } from '../hooks/useRSForms'
import { config } from './constants'
import { type ICurrentUser, type IRSForm, type IUserInfo, type IUserProfile } from './models'
import {
ExpressionParse,
IConstituentaList,
IConstituentaMeta,
ICstCreateData,
ICstCreatedResponse,
ICstMovetoData,
ICstUpdateData,
ICurrentUser, IRSFormCreateData, IRSFormData,
IRSFormMeta, IRSFormUpdateData, IUserInfo, IUserLoginData, IUserProfile, IUserSignupData, RSExpression
} from './models'
export type BackendCallback = (response: AxiosResponse) => void;
// ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
export interface IFrontRequest {
onSuccess?: BackendCallback
interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData
onSuccess?: DataCallback<ResponseData>
onError?: (error: ErrorInfo) => void
setLoading?: (loading: boolean) => void
showError?: boolean
data?: any
}
interface IAxiosRequest {
export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> {
data: DataType
}
export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType>{
onSuccess: DataCallback<DataType>
}
export interface FrontExchange<RequestData, ResponseData> extends IFrontRequest<RequestData, ResponseData>{
data: RequestData
onSuccess: DataCallback<ResponseData>
}
export interface FrontAction extends IFrontRequest<undefined, undefined>{}
interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string
request?: IFrontRequest
title?: string
request: IFrontRequest<RequestData, ResponseData>
title: string
options?: AxiosRequestConfig
}
// ================= Export API ==============
export async function postLogin(request?: IFrontRequest) {
await AxiosPost({
title: 'Login',
endpoint: `${config.url.AUTH}login`,
request
});
}
export async function getAuth(request?: IFrontRequest) {
await AxiosGet<ICurrentUser>({
// ==================== API ====================
export function getAuth(request: FrontPull<ICurrentUser>) {
AxiosGet({
title: 'Current user',
endpoint: `${config.url.AUTH}auth`,
request
request: request
});
}
export async function getProfile(request?: IFrontRequest) {
await AxiosGet<IUserProfile>({
title: 'Current user profile',
endpoint: `${config.url.AUTH}profile`,
request
export function postLogin(request: FrontPush<IUserLoginData>) {
AxiosPost({
title: 'Login',
endpoint: `${config.url.AUTH}login`,
request: request
});
}
export async function postLogout(request?: IFrontRequest) {
await AxiosPost({
export function postLogout(request: FrontAction) {
AxiosPost({
title: 'Logout',
endpoint: `${config.url.AUTH}logout`,
request
request: request
});
};
}
export async function postSignup(request?: IFrontRequest) {
await AxiosPost({
export function postSignup(request: IFrontRequest<IUserSignupData, IUserProfile>) {
AxiosPost({
title: 'Register user',
endpoint: `${config.url.AUTH}signup`,
request
request: request
});
}
export async function getActiveUsers(request?: IFrontRequest) {
await AxiosGet<IUserInfo>({
export function getProfile(request: FrontPull<IUserProfile>) {
AxiosGet({
title: 'Current user profile',
endpoint: `${config.url.AUTH}profile`,
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
AxiosGet({
title: 'Active users list',
endpoint: `${config.url.AUTH}active-users`,
request
request: request
});
}
export async function getRSForms(filter: RSFormsFilter, request?: IFrontRequest) {
let endpoint: string = ''
if (filter.type === FilterType.PERSONAL) {
endpoint = `${config.url.BASE}rsforms?owner=${filter.data as number}`
} else {
endpoint = `${config.url.BASE}rsforms?is_common=true`
}
await AxiosGet<IRSForm[]>({
export function getRSForms(filter: RSFormsFilter, request: FrontPull<IRSFormMeta[]>) {
const endpoint =
filter.type === FilterType.PERSONAL
? `${config.url.BASE}rsforms?owner=${filter.data as number}`
: `${config.url.BASE}rsforms?is_common=true`;
AxiosGet({
title: 'RSForms list',
endpoint,
request
endpoint: endpoint,
request: request
});
}
export async function postNewRSForm(request?: IFrontRequest) {
await AxiosPost({
export function postNewRSForm(request: FrontExchange<IRSFormCreateData, IRSFormMeta>) {
AxiosPost({
title: 'New RSForm',
endpoint: `${config.url.BASE}rsforms/create-detailed/`,
request
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export async function getRSFormDetails(target: string, request?: IFrontRequest) {
await AxiosGet<IRSForm>({
export function getRSFormDetails(target: string, request: FrontPull<IRSFormData>) {
AxiosGet({
title: `RSForm details for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/details/`,
request
request: request
});
}
export async function patchRSForm(target: string, request?: IFrontRequest) {
await AxiosPatch({
export function patchRSForm(target: string, request: FrontExchange<IRSFormUpdateData, IRSFormMeta>) {
AxiosPatch({
title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`,
request
request: request
});
}
export async function patchConstituenta(target: string, request?: IFrontRequest) {
await AxiosPatch({
title: `Constituenta id=${target}`,
endpoint: `${config.url.BASE}constituents/${target}/`,
request
});
}
export async function deleteRSForm(target: string, request?: IFrontRequest) {
await AxiosDelete({
export function deleteRSForm(target: string, request: FrontAction) {
AxiosDelete({
title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`,
request
request: request
});
}
export async function getTRSFile(target: string, request?: IFrontRequest) {
await AxiosGetBlob({
title: `RSForm TRS file for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/export-trs/`,
request
});
}
export async function postClaimRSForm(target: string, request?: IFrontRequest) {
await AxiosPost({
export function postClaimRSForm(target: string, request: FrontPull<IRSFormMeta>) {
AxiosPost({
title: `Claim on RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/claim/`,
request
request: request
});
}
export async function postCheckExpression(schema: string, request?: IFrontRequest) {
await AxiosPost({
title: `Check expression for RSForm id=${schema}: ${request?.data.expression as string}`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
request
export function getTRSFile(target: string, request: FrontPull<Blob>) {
AxiosGet({
title: `RSForm TRS file for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/export-trs/`,
request: request,
options: { responseType: 'blob' }
});
}
export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
await AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data.alias as string}`,
export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request.data.alias}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`,
request
request: request
});
}
export async function patchDeleteConstituenta(schema: string, request?: IFrontRequest) {
await AxiosPatch<IRSForm>({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
title: `Delete Constituents for RSForm id=${schema}: ${request?.data.items.toString()}`,
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
AxiosPatch({
title: `Delete Constituents for RSForm id=${schema}: ${request.data.items.map(item => String(item.id)).join(' ')}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
request
request: request
});
}
export async function patchMoveConstituenta(schema: string, request?: IFrontRequest) {
await AxiosPatch<IRSForm>({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request?.data.items)} to ${request?.data.move_to as number}`,
export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
title: `Constituenta id=${target}`,
endpoint: `${config.url.BASE}constituents/${target}/`,
request: request
});
}
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
AxiosPatch({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request.data.items)} to ${request.data.move_to}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-moveto/`,
request
request: request
});
}
// ====== Helper functions ===========
async function AxiosGet<ReturnType>({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`);
if (request?.setLoading) request?.setLoading(true);
axios.get<ReturnType>(endpoint)
export function postCheckExpression(schema: string, request: FrontExchange<RSExpression, ExpressionParse>) {
AxiosPost({
title: `Check expression for RSForm id=${schema}: ${request.data.expression }`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
request: request
});
}
// ============ Helper functions =============
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
console.log(`[[${title}]] requested`);
if (request.setLoading) request?.setLoading(true);
axios.get<ResponseData>(endpoint, options)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSuccess) request.onSuccess(response);
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
async function AxiosGetBlob({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`);
if (request?.setLoading) request?.setLoading(true);
axios.get(endpoint, { responseType: 'blob' })
function AxiosPost<RequestData, ResponseData>(
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
) {
console.log(`[[${title}]] posted`);
if (request.setLoading) request.setLoading(true);
axios.post<ResponseData>(endpoint, request.data, options)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSuccess) request.onSuccess(response);
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
async function AxiosPost({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] posted`);
if (request?.setLoading) request?.setLoading(true);
axios.post(endpoint, request?.data)
function AxiosDelete<RequestData, ResponseData>(
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
) {
console.log(`[[${title}]] is being deleted`);
if (request.setLoading) request.setLoading(true);
axios.delete<ResponseData>(endpoint, options)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSuccess) request.onSuccess(response);
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}
async function AxiosDelete({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being deleted`);
if (request?.setLoading) request?.setLoading(true);
axios.delete(endpoint)
function AxiosPatch<RequestData, ResponseData>(
{ endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
) {
console.log(`[[${title}]] is being patrially updated`);
if (request.setLoading) request.setLoading(true);
axios.patch<ResponseData>(endpoint, request.data, options)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSuccess) request.onSuccess(response);
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
});
}
async function AxiosPatch<ReturnType>({ endpoint, request, title }: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being patrially updated`);
if (request?.setLoading) request?.setLoading(true);
axios.patch<ReturnType>(endpoint, request?.data)
.then((response) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.onSuccess) request.onSuccess(response);
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
return response.data;
})
.catch((error) => {
if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error);
if (request.setLoading) request.setLoading(false);
if (request.showError) toast.error(error.message);
if (request.onError) request.onError(error);
});
}

View File

@ -0,0 +1,160 @@
//! RS language token types enumeration
export enum TokenID {
// Global, local IDs and literals
ID_LOCAL = 258,
ID_GLOBAL,
ID_FUNCTION,
ID_PREDICATE,
ID_RADICAL,
LIT_INTEGER,
LIT_INTSET,
LIT_EMPTYSET,
// Aithmetic
PLUS,
MINUS,
MULTIPLY,
// Integer predicate symbols
GREATER,
LESSER,
GREATER_OR_EQ,
LESSER_OR_EQ,
// Equality comparison
EQUAL,
NOTEQUAL,
// Logic predicate symbols
FORALL,
EXISTS,
NOT,
EQUIVALENT,
IMPLICATION,
OR,
AND,
// Set theory predicate symbols
IN,
NOTIN,
SUBSET,
SUBSET_OR_EQ,
NOTSUBSET,
// Set theory operators
DECART,
UNION,
INTERSECTION,
SET_MINUS,
SYMMINUS,
BOOLEAN,
// Structure operations
BIGPR,
SMALLPR,
FILTER,
CARD,
BOOL,
DEBOOL,
REDUCE,
// Term constructions prefixes
DECLARATIVE,
RECURSIVE,
IMPERATIVE,
// Punctuation
PUNC_DEFINE,
PUNC_STRUCT,
PUNC_ASSIGN,
PUNC_ITERATE,
PUNC_PL,
PUNC_PR,
PUNC_CL,
PUNC_CR,
PUNC_SL,
PUNC_SR,
PUNC_BAR,
PUNC_COMMA,
PUNC_SEMICOLON,
// ======= Non-terminal tokens =========
NT_ENUM_DECL, // Перечисление переменных в кванторной декларации
NT_TUPLE, // Кортеж (a,b,c), типизация B(T(a)xT(b)xT(c))
NT_ENUMERATION, // Задание множества перечислением
NT_TUPLE_DECL, // Декларация переменных с помощью кортежа
NT_ARG_DECL, // Объявление аргумента
NT_FUNC_DEFINITION, // Определение функции
NT_ARGUMENTS, // Задание аргументов функции
NT_FUNC_CALL, // Вызов функции
NT_DECLARATIVE_EXPR, // Задание множества с помощью выражения D{x из H | A(x) }
NT_IMPERATIVE_EXPR, // Императивное определение
NT_RECURSIVE_FULL, // Полная рекурсия
NT_RECURSIVE_SHORT, // Сокращенная рекурсия
NT_IMP_DECLARE, // Блок декларации
NT_IMP_ASSIGN, // Блок присвоения
NT_IMP_LOGIC, // Блок проверки
// ======= Helper tokens ========
INTERRUPT,
END,
}
export enum RSError {
}
// '8201': 'Число превышает максимально допустимое значение 2147483647!',
// '8203': 'Нераспознанный символ!',
// '8400': 'Неопределенная синтаксическая ошибка!',
// '8406': 'Пропущена скобка )!',
// '8407': 'Пропущена скобка }!',
// '8408': 'Некорректная кванторная декларация переменной!',
// '8414': 'Некорректное объявление аргументов функции!',
// '8415': 'Некорректное имя локальной переменной в декларации функции!',
// '2801': 'Повторное объявление локальной переменной!',
// '2802': 'Локальная переменная объявлена, но не использована!',
// '8801': 'Использование необъявленной локальной переменной!',
// '8802': 'Повторное объявление локальной переменной внутри области действия!',
// '8803': 'Типизация операндов не совпадает!',
// '8804': 'Использована конституента с неопределенной типизацией!',
// '8805': 'Одна из проекций декартова произведения не является типизированным множеством имеющим характер множества!',
// '8806': 'Аргумент булеана не является типизированным множеством имеющим характер множества!',
// '8807': 'Операнд теоретико-множественного оператора не является типизированным множеством имеющим характер множества!',
// '8808': 'Операнд оператора card не является типизированным множеством имеющим характер множества!',
// '8809': 'Операнд оператора debool не является типизированным множеством имеющим характер множества!',
// '880A': 'Неизвестное имя функции!',
// '880B': 'Некорректное использование имени функции без аргументов!',
// '8810': 'Операнд оператора red не является типизированным множеством имеющим характер двойного булеана!',
// '8811': 'Некорректная типизация аргумента: проекция не определена!',
// '8812': 'Некорректная типизация аргумента: T(Pri(a)) = B(Pi(D(T(a))))!',
// '8813': 'Типизация элементов перечисления не совпадает!',
// '8814': 'Некорректная декларация связанных локальных переменных: количестве переменных в кортеже не соответствует размерности декартова произведения типизации!',
// '8815': 'Локальная переменная используется вне области действия!',
// '8816': 'Несоответствие типизаций операндов для предиката!',
// '8818': 'Некорректное количество аргументов терм-функции!',
// '8819': 'Типизация аргумента терм-функции не совпадает с объявленной!',
// '881A': 'Сравнение кортежа или элемента с пустым множеством!',
// '881C': 'Выражение родовой структуры должно быть ступенью!',
// '881F': 'Ожидалось выражение объявления функции!',
// '8820': 'Некорректное использование пустого множества как типизированного выражения!',
// '8821': 'Радикалы запрещены вне деклараций терм-функций!',
// '8822': 'Типизация аргумента фильтра не корректна!',
// '8823': 'Количество параметров фильтра не соответствует количеству индексов!',
// '8824': 'Для выбранного типа не поддерживаются арифметические операции!',
// '8825': 'Типизации не совместимы для выбранной операции/предиката!',
// '8826': 'Для выбранного типа не поддерживаются предикаты порядка!',
// '8840': 'Используется неинтерпретируемый глобальный идентификатор!',
// '8841': 'Использование свойства в качестве значения!',
// '8842': 'Не удалось получить дерево разбора для глобального идентификатора!',
// '8843': 'Функция не интерпретируется для данных аргументов!',
// '8A00': 'Неизвестная ошибка: вычисление прервано!',
// '8A01': 'Превышен пределен количества элементов множества!',
// '8A02': 'Превышен пределен количества элементов в основании булеана!',
// '8A03': 'Использование конституенты с неопределенным значением!',
// '8A04': 'Превышен предел количества итераций!',
// '8A05': 'Попытка взять debool от многоэлементного множества!',
// '8A06': 'Попытка перебрать бесконечное множество!'

View File

@ -1,56 +1,27 @@
// Current user info
export interface ICurrentUser {
id: number
// ========= Users ===========
export interface IUser {
id: number | null
username: string
is_staff: boolean
}
// User profile data
export interface IUserProfile {
id: number
username: string
email: string
first_name: string
last_name: string
}
// User base info
export interface IUserInfo {
id: number
username: string
first_name: string
last_name: string
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {}
export interface IUserLoginData extends Pick<IUser, 'username'> {
password: string
}
// User data for signup
export interface IUserSignupData {
username: string
email: string
first_name: string
last_name: string
export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
password: string
password2: string
}
export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
export interface IUserInfo extends Omit<IUserProfile, 'email'> {}
// User data for signup
export interface INewCstData {
alias: string
csttype: CstType
insert_after?: number
}
// Constituenta type
export enum CstType {
BASE = 'basic',
CONSTANT = 'constant',
STRUCTURED = 'structure',
AXIOM = 'axiom',
TERM = 'term',
FUNCTION = 'function',
PREDICATE = 'predicate',
THEOREM = 'theorem'
}
// ======== Parsing ============
// ValueClass
export enum ValueClass {
INVALID = 'invalid',
@ -72,25 +43,56 @@ export enum ParsingStatus {
INCORRECT = 'incorrect'
}
// Constituenta data
export interface RSErrorDescription {
errorType: number
position: number
isCritical: boolean
params: string[]
}
export interface ExpressionParse {
parseResult: boolean
syntax: Syntax
typification: string
valueClass: ValueClass
astText: string
errors: RSErrorDescription[]
}
export interface RSExpression {
expression: string
}
// ====== Constituenta ==========
export enum CstType {
BASE = 'basic',
STRUCTURED = 'structure',
TERM = 'term',
AXIOM = 'axiom',
FUNCTION = 'function',
PREDICATE = 'predicate',
CONSTANT = 'constant',
THEOREM = 'theorem'
}
export interface IConstituenta {
id: number
alias: string
cstType: CstType
convention?: string
term?: {
convention: string
term: {
raw: string
resolved?: string
forms?: string[]
resolved: string
forms: string[]
}
definition?: {
definition: {
formal: string
text: {
raw: string
resolved?: string
resolved: string
}
}
parse?: {
parse: {
status: ParsingStatus
valueClass: ValueClass
typification: string
@ -98,7 +100,42 @@ export interface IConstituenta {
}
}
// RSForm stats
export interface IConstituentaMeta {
id: number
schema: number
order: number
alias: string
convention: string
cst_type: CstType
definition_formal: string
definition_raw: string
definition_resolved: string
term_raw: string
term_resolved: string
}
export interface IConstituentaID extends Pick<IConstituentaMeta, 'id'>{}
export interface IConstituentaList {
items: IConstituentaID[]
}
export interface ICstCreateData extends Pick<IConstituentaMeta, 'alias' | 'cst_type'> {
insert_after: number | null
}
export interface ICstMovetoData extends IConstituentaList {
move_to: number
}
export interface ICstUpdateData
extends Pick<IConstituentaMeta, 'id' | 'alias' | 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw'> {}
export interface ICstCreatedResponse {
new_cst: IConstituentaMeta
schema: IRSFormData
}
// ========== RSForm ============
export interface IRSFormStats {
count_all: number
count_errors: number
@ -117,7 +154,6 @@ export interface IRSFormStats {
count_theorem: number
}
// RSForm data
export interface IRSForm {
id: number
title: string
@ -126,125 +162,24 @@ export interface IRSForm {
is_common: boolean
time_create: string
time_update: string
owner?: number
items?: IConstituenta[]
stats?: IRSFormStats
owner: number | null
items: IConstituenta[]
stats: IRSFormStats
}
// RSForm user input
export interface IRSFormCreateData {
title: string
alias: string
comment: string
is_common: boolean
export interface IRSFormData extends Omit<IRSForm, 'stats' > {}
export interface IRSFormMeta extends Omit<IRSForm, 'items' | 'stats'> {}
export interface IRSFormUpdateData
extends Omit<IRSFormMeta, 'time_create' | 'time_update' | 'id' | 'owner'> {}
export interface IRSFormCreateData
extends IRSFormUpdateData {
file?: File
fileName?: string
}
//! RS language token types enumeration
export enum TokenID {
// Global, local IDs and literals
ID_LOCAL = 258,
ID_GLOBAL,
ID_FUNCTION,
ID_PREDICATE,
ID_RADICAL,
LIT_INTEGER,
LIT_INTSET,
LIT_EMPTYSET,
// Aithmetic
PLUS,
MINUS,
MULTIPLY,
// Integer predicate symbols
GREATER,
LESSER,
GREATER_OR_EQ,
LESSER_OR_EQ,
// Equality comparison
EQUAL,
NOTEQUAL,
// Logic predicate symbols
FORALL,
EXISTS,
NOT,
EQUIVALENT,
IMPLICATION,
OR,
AND,
// Set theory predicate symbols
IN,
NOTIN,
SUBSET,
SUBSET_OR_EQ,
NOTSUBSET,
// Set theory operators
DECART,
UNION,
INTERSECTION,
SET_MINUS,
SYMMINUS,
BOOLEAN,
// Structure operations
BIGPR,
SMALLPR,
FILTER,
CARD,
BOOL,
DEBOOL,
REDUCE,
// Term constructions prefixes
DECLARATIVE,
RECURSIVE,
IMPERATIVE,
// Punctuation
PUNC_DEFINE,
PUNC_STRUCT,
PUNC_ASSIGN,
PUNC_ITERATE,
PUNC_PL,
PUNC_PR,
PUNC_CL,
PUNC_CR,
PUNC_SL,
PUNC_SR,
PUNC_BAR,
PUNC_COMMA,
PUNC_SEMICOLON,
// ======= Non-terminal tokens =========
NT_ENUM_DECL, // Перечисление переменных в кванторной декларации
NT_TUPLE, // Кортеж (a,b,c), типизация B(T(a)xT(b)xT(c))
NT_ENUMERATION, // Задание множества перечислением
NT_TUPLE_DECL, // Декларация переменных с помощью кортежа
NT_ARG_DECL, // Объявление аргумента
NT_FUNC_DEFINITION, // Определение функции
NT_ARGUMENTS, // Задание аргументов функции
NT_FUNC_CALL, // Вызов функции
NT_DECLARATIVE_EXPR, // Задание множества с помощью выражения D{x из H | A(x) }
NT_IMPERATIVE_EXPR, // Императивное определение
NT_RECURSIVE_FULL, // Полная рекурсия
NT_RECURSIVE_SHORT, // Сокращенная рекурсия
NT_IMP_DECLARE, // Блок декларации
NT_IMP_ASSIGN, // Блок присвоения
NT_IMP_LOGIC, // Блок проверки
// ======= Helper tokens ========
INTERRUPT,
END,
};
// ================ Misc types ================
// Constituenta edit mode
export enum EditMode {
TEXT = 'text',
@ -280,9 +215,10 @@ export function inferStatus(parse?: ParsingStatus, value?: ValueClass): Expressi
return ExpressionStatus.VERIFIED
}
export function CalculateStats(schema: IRSForm) {
if (!schema.items) {
schema.stats = {
export function LoadRSFormData(schema: IRSFormData): IRSForm {
const result = schema as IRSForm
if (!result.items) {
result.stats = {
count_all: 0,
count_errors: 0,
count_property: 0,
@ -299,22 +235,22 @@ export function CalculateStats(schema: IRSForm) {
count_predicate: 0,
count_theorem: 0
}
return;
return result;
}
schema.stats = {
count_all: schema.items?.length || 0,
count_errors: schema.items?.reduce(
result.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(
count_property: schema.items.reduce(
(sum, cst) => sum + (cst.parse?.valueClass === ValueClass.PROPERTY ? 1 : 0) || 0, 0),
count_incalc: schema.items?.reduce(
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(
count_termin: schema.items.reduce(
(sum, cst) => (sum + (cst.term?.raw ? 1 : 0) || 0), 0),
count_base: schema.items?.reduce(
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),
@ -322,15 +258,16 @@ export function CalculateStats(schema: IRSForm) {
(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(
count_term: schema.items.reduce(
(sum, cst) => sum + (cst.cstType === CstType.TERM ? 1 : 0), 0),
count_function: schema.items?.reduce(
count_function: schema.items.reduce(
(sum, cst) => sum + (cst.cstType === CstType.FUNCTION ? 1 : 0), 0),
count_predicate: schema.items?.reduce(
count_predicate: schema.items.reduce(
(sum, cst) => sum + (cst.cstType === CstType.PREDICATE ? 1 : 0), 0),
count_theorem: schema.items?.reduce(
count_theorem: schema.items.reduce(
(sum, cst) => sum + (cst.cstType === CstType.THEOREM ? 1 : 0), 0)
}
return result;
}
export function matchConstituenta(query: string, target?: IConstituenta) {

View File

@ -1,7 +1,8 @@
import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { type BackendCallback } from './backendAPI';
import { type DataCallback } from './backendAPI';
import { IRSFormMeta } from './models';
export function shareCurrentURLProc() {
const url = window.location.href + '&share';
@ -11,7 +12,7 @@ export function shareCurrentURLProc() {
}
export function claimOwnershipProc(
claim: (callback: BackendCallback) => void
claim: (callback: DataCallback<IRSFormMeta>) => void
) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
@ -20,7 +21,7 @@ export function claimOwnershipProc(
}
export function deleteRSFormProc(
destroy: (callback: BackendCallback) => void,
destroy: (callback: DataCallback) => void,
navigate: (path: string) => void
) {
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
@ -33,14 +34,14 @@ export function deleteRSFormProc(
}
export function downloadRSFormProc(
download: (callback: BackendCallback) => void,
download: (callback: DataCallback<Blob>) => void,
fileName: string
) {
download((response) => {
download((data) => {
try {
fileDownload(response.data, fileName);
} catch (error: any) {
toast.error(error.message);
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}

View File

@ -1,4 +1,5 @@
import { CstType, ExpressionStatus, type IConstituenta, type IRSForm, ParsingStatus, TokenID } from './models';
import { TokenID } from './enums';
import { CstType, ExpressionStatus, type IConstituenta, type IRSForm, ParsingStatus, ValueClass } from './models';
export interface IRSButtonData {
text: string
@ -195,6 +196,20 @@ export function getCstTypeLabel(type: CstType) {
}
}
export function getCstTypeShortcut(type: CstType) {
const prefix = getCstTypeLabel(type) + ' [Alt + ';
switch (type) {
case CstType.BASE: return prefix + '1]';
case CstType.STRUCTURED: return prefix + '2]';
case CstType.TERM: return prefix + '3]';
case CstType.AXIOM: return prefix + '4]';
case CstType.FUNCTION: return prefix + 'Q]';
case CstType.PREDICATE: return prefix + 'W]';
case CstType.CONSTANT: return prefix + '5]';
case CstType.THEOREM: return prefix + '6]';
}
}
export const CstTypeSelector = (Object.values(CstType)).map(
(typeStr) => {
const type = typeStr as CstType;
@ -272,3 +287,30 @@ export function createAliasFor(type: CstType, schema: IRSForm): string {
}, 1);
return `${prefix}${index}`;
}
export function getMockConstituenta(id: number, alias: string, type: CstType, comment: string): IConstituenta {
return {
id: id,
alias: alias,
convention: comment,
cstType: type,
term: {
raw: '',
resolved: '',
forms: []
},
definition: {
formal: '',
text: {
raw: '',
resolved: ''
}
},
parse: {
status: ParsingStatus.INCORRECT,
valueClass: ValueClass.INVALID,
typification: 'N/A',
syntaxTree: ''
}
};
}

1
rsconcept/frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -1,5 +1,5 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
export default {
darkMode: 'class',
content: [
'./src/**/*.{js,jsx,ts,tsx}',

View File

@ -1,26 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,10 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3000
}
})