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 RunBackend
RunFrontend RunFrontend
Start-Sleep -Seconds 1 Start-Sleep -Seconds 1
Start-Process "http://127.0.0.1:8000/" Start-Process "http://localhost:8000/"
Start-Process "http://localhost:3000/"
} }
function RunBackend() { function RunBackend() {
@ -29,7 +30,8 @@ function RunBackend() {
function RunFrontend() { function RunFrontend() {
Set-Location $PSScriptRoot\frontend 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 { function FlushData {

View File

@ -1,6 +1,8 @@
# Application settings # Application settings
SECRET_KEY=django-insecure-)rq@!&v7l2r%2%q#n!uq+zk@=&yc0^&ql^7%2!%9u)vt1x&j=d 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 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 # 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 import apps.rsform.models
from django.conf import settings 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')), ('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='Позиция')), ('order', models.PositiveIntegerField(default=-1, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Позиция')),
('alias', models.CharField(default='undefined', max_length=8, 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='Комментарий/Конвенция')), ('convention', models.TextField(blank=True, default='', verbose_name='Комментарий/Конвенция')),
('term_raw', 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='Термин')), ('term_resolved', models.TextField(blank=True, default='', verbose_name='Термин')),

View File

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

View File

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

View File

@ -53,7 +53,7 @@ class TestConstituenta(TestCase):
self.assertEqual(cst.schema, self.schema1) self.assertEqual(cst.schema, self.schema1)
self.assertEqual(cst.order, 1) self.assertEqual(cst.order, 1)
self.assertEqual(cst.alias, 'X1') 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.convention, '')
self.assertEqual(cst.definition_formal, '') self.assertEqual(cst.definition_formal, '')
self.assertEqual(cst.term_raw, '') self.assertEqual(cst.term_raw, '')

View File

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

View File

@ -56,16 +56,18 @@ class RSFormViewSet(viewsets.ModelViewSet):
schema: models.RSForm = self.get_object() schema: models.RSForm = self.get_object()
serializer = serializers.CstCreateSerializer(data=request.data) serializer = serializers.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
if ('insert_after' in serializer.validated_data): 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']) cstafter = models.Constituenta.objects.get(pk=serializer.validated_data['insert_after'])
constituenta = schema.insert_at(cstafter.order + 1, constituenta = schema.insert_at(cstafter.order + 1,
serializer.validated_data['alias'], serializer.validated_data['alias'],
serializer.validated_data['csttype']) serializer.validated_data['cst_type'])
else: 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() schema.refresh_from_db()
outSerializer = serializers.RSFormDetailsSerlializer(schema) 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() response['Location'] = constituenta.get_absolute_url()
return response return response
@ -175,13 +177,15 @@ def create_rsform(request):
data = utils.read_trs(request.FILES['file'].file) data = utils.read_trs(request.FILES['file'].file)
if ('title' in request.data and request.data['title'] != ''): if ('title' in request.data and request.data['title'] != ''):
data['title'] = 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'] != ''): if ('alias' in request.data and request.data['alias'] != ''):
data['alias'] = request.data['alias'] data['alias'] = request.data['alias']
if ('comment' in request.data and request.data['comment'] != ''): if ('comment' in request.data and request.data['comment'] != ''):
data['comment'] = request.data['comment'] data['comment'] = request.data['comment']
is_common = True is_common = True
if ('is_common' in request.data): 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) schema = models.RSForm.import_json(owner, data, is_common)
result = serializers.RSFormSerializer(schema) result = serializers.RSFormSerializer(schema)
return Response(status=201, data=result.data) return Response(status=201, data=result.data)

View File

@ -64,12 +64,10 @@ REST_FRAMEWORK = {
], ],
} }
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = 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(';') 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 = [ MIDDLEWARE = [

View File

@ -4,28 +4,27 @@
"es2021": true "es2021": true
}, },
"extends": [ "extends": [
"standard-with-typescript", "eslint:recommended",
"plugin:react/recommended", "plugin:@typescript-eslint/recommended-type-checked",
"plugin:react/jsx-runtime" "plugin:react-hooks/recommended"
], ],
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module", "sourceType": "module",
"project": ["tsconfig.json"] "project": ["tsconfig.json", "tsconfig.node.json"]
}, },
"plugins": [ "plugins": [
"react", "simple-import-sort" "react-refresh", "simple-import-sort"
], ],
"rules": { "rules": {
"react-refresh/only-export-components": [
"off",
{ "allowConstantExport": true }
],
"simple-import-sort/imports": "warn", "simple-import-sort/imports": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"no-trailing-spaces": "warn", "@typescript-eslint/no-unsafe-member-access": "off",
"no-multiple-empty-lines": "warn", "@typescript-eslint/no-unsafe-argument": "off"
"@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"
} }
} }

View File

@ -1,23 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Logs
logs
# dependencies *.log
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.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 ======= # ======= Build =======
FROM node-base as builder FROM node-base as builder
ENV NODE_ENV production
WORKDIR /result WORKDIR /result
# Install dependencies COPY ./ ./
COPY *.json *.js ./ RUN npm install
RUN npm ci --only=production
# Build deployment files
COPY ./public ./public
COPY ./src ./src
RUN npm run build RUN npm run build
# ========= Server ======= # ========= Server =======
@ -33,7 +27,7 @@ USER node
# Bring up deployment files # Bring up deployment files
WORKDIR /home/node WORKDIR /home/node
COPY --chown=node:node --from=builder /result/build ./ COPY --chown=node:node --from=builder /result/dist ./
# Start server through docker-compose # Start server through docker-compose
# serve -s /home/node -l 3000 # serve -s /home/node -l 3000

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.1.0",
"private": true, "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": { "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", "axios": "^1.4.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.2.0", "react": "^18.2.0",
@ -18,42 +18,26 @@
"react-error-boundary": "^4.0.10", "react-error-boundary": "^4.0.10",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
"react-loader-spinner": "^5.3.4", "react-loader-spinner": "^5.3.4",
"react-router-dom": "^6.12.1", "react-router-dom": "^6.14.2",
"react-scripts": "^5.0.1",
"react-select": "^5.7.4", "react-select": "^5.7.4",
"react-tabs": "^6.0.1", "react-tabs": "^6.0.2",
"react-toastify": "^9.1.3", "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"
]
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.0", "@types/node": "^20.4.5",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@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": "^8.45.0",
"eslint-config-standard-with-typescript": "^37.0.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-react-refresh": "^0.4.3",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-simple-import-sort": "^10.0.0", "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'; 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 ( return (
<div className='flex flex-col justify-start'> <div className='flex flex-col justify-start'>
<p className='underline'>Ошибка</p> <p className='underline'>Ошибка</p>
<p>{error.message}</p> <p>{error.message}</p>
{error.response.data && (<> {error.response.data && (<>
<p className='mt-2 underline'>Описание</p> <p className='mt-2 underline'>Описание</p>
{ isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data }} /> } { isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> }
{ !isHtml && <PrettyJson data={error.response.data} />} { !isHtml && <PrettyJson data={error.response.data as object} />}
</>)} </>)}
</div> </div>
); );

View File

@ -4,7 +4,8 @@ import { Tab } from 'react-tabs';
function ConceptTab({ children, className, ...otherProps }: TabProps) { function ConceptTab({ children, className, ...otherProps }: TabProps) {
return ( return (
<Tab <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} {...otherProps}
> >
{children} {children}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,21 @@
import { useState } from 'react' import { useState } from 'react'
import { type ErrorInfo } from '../components/BackendError'; 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() { function useNewRSForm() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
async function createSchema({ data, file, onSuccess }: { function createSchema(data: IRSFormCreateData, onSuccess: DataCallback<IRSFormMeta>) {
data: any
file?: File
onSuccess: (newID: string) => void
}) {
setError(undefined); setError(undefined);
if (file) { postNewRSForm({
data.file = file; data: data,
data.fileName = file.name;
}
await postNewRSForm({
data,
showError: true, showError: true,
setLoading, setLoading: setLoading,
onError: error => { setError(error); }, 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 { type ErrorInfo } from '../components/BackendError';
import { getRSFormDetails } from '../utils/backendAPI'; 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 }) { export function useRSFormDetails({ target }: { target?: string }) {
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined); const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
function setSchema(schema?: IRSForm) { function setSchema(data?: IRSFormData) {
if (schema) CalculateStats(schema); if (!data) {
setInnerSchema(undefined);
return;
}
const schema = LoadRSFormData(data);
setInnerSchema(schema); setInnerSchema(schema);
console.log('Loaded schema: ', schema); console.log('Loaded schema: ', schema);
} }
const fetchData = useCallback( const reload = useCallback(
async (setCustomLoading?: typeof setLoading) => { (setCustomLoading?: typeof setLoading, callback?: () => void) => {
setError(undefined); setError(undefined);
if (!target) { if (!target) {
return; return;
} }
await getRSFormDetails(target, { getRSFormDetails(target, {
showError: true, showError: true,
setLoading: setCustomLoading ?? setLoading, setLoading: setCustomLoading ?? setLoading,
onError: error => { setInnerSchema(undefined); setError(error); }, onError: error => { setInnerSchema(undefined); setError(error); },
onSuccess: (response) => { setSchema(response.data); } onSuccess: schema => {
setSchema(schema);
if (callback) callback();
}
}); });
}, [target]); }, [target]);
async function reload(setCustomLoading?: typeof setLoading) {
await fetchData(setCustomLoading);
}
useEffect(() => { useEffect(() => {
fetchData().catch((error) => { setError(error); }); reload();
}, [fetchData]) }, [reload])
return { schema, setSchema, reload, error, setError, loading }; 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 { type ErrorInfo } from '../components/BackendError';
import { getRSForms } from '../utils/backendAPI'; import { getRSForms } from '../utils/backendAPI';
import { type IRSForm } from '../utils/models' import { IRSFormMeta } from '../utils/models'
export enum FilterType { export enum FilterType {
PERSONAL = 'personal', PERSONAL = 'personal',
@ -11,20 +11,20 @@ export enum FilterType {
export interface RSFormsFilter { export interface RSFormsFilter {
type: FilterType type: FilterType
data?: any data?: number | null
} }
export function useRSForms() { export function useRSForms() {
const [rsforms, setRSForms] = useState<IRSForm[]>([]); const [rsforms, setRSForms] = useState<IRSFormMeta[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const loadList = useCallback(async (filter: RSFormsFilter) => { const loadList = useCallback((filter: RSFormsFilter) => {
await getRSForms(filter, { getRSForms(filter, {
showError: true, showError: true,
setLoading, setLoading,
onError: error => { setError(error); }, 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 [error, setError] = useState<ErrorInfo>(undefined);
const fetchUser = useCallback( const fetchUser = useCallback(
async () => { () => {
setError(undefined); setError(undefined);
setUser(undefined); setUser(undefined);
getProfile({
await getProfile({
showError: true, showError: true,
setLoading, setLoading: setLoading,
onError: error => { setError(error); }, onError: error => { setError(error); },
onSuccess: response => { setUser(response.data); } onSuccess: newData => { setUser(newData); }
}); });
}, [setUser] }, [setUser]
) )
useEffect(() => { useEffect(() => {
fetchUser().catch((error) => { setError(error); }); fetchUser();
}, [fetchUser]) }, [fetchUser])
return { user, fetchUser, error, loading }; return { user, fetchUser, error, loading };

View File

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

View File

@ -8,6 +8,7 @@ import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage'; import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { IUserLoginData } from '../utils/models';
function LoginPage() { function LoginPage() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -30,7 +31,11 @@ function LoginPage() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!loading) { 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 TextInput from '../components/Common/TextInput';
import RequireAuth from '../components/RequireAuth'; import RequireAuth from '../components/RequireAuth';
import useNewRSForm from '../hooks/useNewRSForm'; import useNewRSForm from '../hooks/useNewRSForm';
import { type IRSFormCreateData } from '../utils/models'; import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
function RSFormCreatePage() { function RSFormCreatePage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -30,9 +30,9 @@ function RSFormCreatePage() {
} }
} }
const onSuccess = (newID: string) => { const onSuccess = (newSchema: IRSFormMeta) => {
toast.success('Схема успешно создана'); toast.success('Схема успешно создана');
navigate(`/rsforms/${newID}`); navigate(`/rsforms/${newSchema.id}`);
} }
const { createSchema, error, setError, loading } = useNewRSForm() const { createSchema, error, setError, loading } = useNewRSForm()
@ -46,16 +46,14 @@ function RSFormCreatePage() {
return; return;
} }
const data: IRSFormCreateData = { const data: IRSFormCreateData = {
title, title: title,
alias, alias: alias,
comment, comment: comment,
is_common: common is_common: common,
file: file,
fileName: file?.name
}; };
void createSchema({ void createSchema(data, onSuccess);
data,
file,
onSuccess
});
}; };
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { useMemo } from 'react'; 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'; import { getStatusInfo } from '../../utils/staticUI';
interface StatusBarProps { interface StatusBarProps {
isModified?: boolean isModified?: boolean
parseData?: any parseData?: ExpressionParse
constituenta?: IConstituenta constituenta?: IConstituenta
} }

View File

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

View File

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

View File

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

View File

@ -1,15 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Form from '../components/Common/Form'; import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL';
import InfoMessage from '../components/InfoMessage'; import InfoMessage from '../components/InfoMessage';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { type IUserSignupData } from '../utils/models'; import { type IUserSignupData } from '../utils/models';
function RegisterPage() { function RegisterPage() {
const navigate = useNavigate();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -17,7 +19,6 @@ function RegisterPage() {
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const [success, setSuccess] = useState(false);
const { user, signup, loading, error, setError } = useAuth() const { user, signup, loading, error, setError } = useAuth()
useEffect(() => { useEffect(() => {
@ -35,20 +36,18 @@ function RegisterPage() {
first_name: firstName, first_name: firstName,
last_name: lastName last_name: lastName
}; };
signup(data, () => { setSuccess(true); }); signup(data, createdUser => {
navigate(`/login?username=${createdUser.username}`);
toast.success(`Пользователь успешно создан: ${createdUser.username}`);
});
} }
}; };
return ( return (
<div className='w-full py-2'> <div className='w-full py-2'>
{ success && { user &&
<div className='flex flex-col items-center'>
<InfoMessage message={`Вы успешно зарегистрировали пользователя ${username}`}/>
<TextURL text='Войти в аккаунт' href={`/login?username=${username}`}/>
</div>}
{ !success && user &&
<InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> } <InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> }
{ !success && !user && { !user &&
<Form title='Регистрация пользователя' onSubmit={handleSubmit}> <Form title='Регистрация пользователя' onSubmit={handleSubmit}>
<TextInput id='username' label='Имя пользователя' type='text' <TextInput id='username' label='Имя пользователя' type='text'
required 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 { toast } from 'react-toastify'
import { type ErrorInfo } from '../components/BackendError' import { type ErrorInfo } from '../components/BackendError'
import { FilterType, type RSFormsFilter } from '../hooks/useRSForms' import { FilterType, RSFormsFilter } from '../hooks/useRSForms'
import { config } from './constants' 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 { interface IFrontRequest<RequestData, ResponseData> {
onSuccess?: BackendCallback data?: RequestData
onSuccess?: DataCallback<ResponseData>
onError?: (error: ErrorInfo) => void onError?: (error: ErrorInfo) => void
setLoading?: (loading: boolean) => void setLoading?: (loading: boolean) => void
showError?: boolean 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 endpoint: string
request?: IFrontRequest request: IFrontRequest<RequestData, ResponseData>
title?: string title: string
options?: AxiosRequestConfig
} }
// ================= Export API ============== // ==================== API ====================
export async function postLogin(request?: IFrontRequest) { export function getAuth(request: FrontPull<ICurrentUser>) {
await AxiosPost({ AxiosGet({
title: 'Login',
endpoint: `${config.url.AUTH}login`,
request
});
}
export async function getAuth(request?: IFrontRequest) {
await AxiosGet<ICurrentUser>({
title: 'Current user', title: 'Current user',
endpoint: `${config.url.AUTH}auth`, endpoint: `${config.url.AUTH}auth`,
request request: request
}); });
} }
export async function getProfile(request?: IFrontRequest) { export function postLogin(request: FrontPush<IUserLoginData>) {
await AxiosGet<IUserProfile>({ AxiosPost({
title: 'Current user profile', title: 'Login',
endpoint: `${config.url.AUTH}profile`, endpoint: `${config.url.AUTH}login`,
request request: request
}); });
} }
export async function postLogout(request?: IFrontRequest) { export function postLogout(request: FrontAction) {
await AxiosPost({ AxiosPost({
title: 'Logout', title: 'Logout',
endpoint: `${config.url.AUTH}logout`, endpoint: `${config.url.AUTH}logout`,
request request: request
}); });
}; }
export async function postSignup(request?: IFrontRequest) { export function postSignup(request: IFrontRequest<IUserSignupData, IUserProfile>) {
await AxiosPost({ AxiosPost({
title: 'Register user', title: 'Register user',
endpoint: `${config.url.AUTH}signup`, endpoint: `${config.url.AUTH}signup`,
request request: request
}); });
} }
export async function getActiveUsers(request?: IFrontRequest) { export function getProfile(request: FrontPull<IUserProfile>) {
await AxiosGet<IUserInfo>({ AxiosGet({
title: 'Current user profile',
endpoint: `${config.url.AUTH}profile`,
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
AxiosGet({
title: 'Active users list', title: 'Active users list',
endpoint: `${config.url.AUTH}active-users`, endpoint: `${config.url.AUTH}active-users`,
request request: request
}); });
} }
export async function getRSForms(filter: RSFormsFilter, request?: IFrontRequest) { export function getRSForms(filter: RSFormsFilter, request: FrontPull<IRSFormMeta[]>) {
let endpoint: string = '' const endpoint =
if (filter.type === FilterType.PERSONAL) { filter.type === FilterType.PERSONAL
endpoint = `${config.url.BASE}rsforms?owner=${filter.data as number}` ? `${config.url.BASE}rsforms?owner=${filter.data as number}`
} else { : `${config.url.BASE}rsforms?is_common=true`;
endpoint = `${config.url.BASE}rsforms?is_common=true` AxiosGet({
}
await AxiosGet<IRSForm[]>({
title: 'RSForms list', title: 'RSForms list',
endpoint, endpoint: endpoint,
request request: request
}); });
} }
export async function postNewRSForm(request?: IFrontRequest) { export function postNewRSForm(request: FrontExchange<IRSFormCreateData, IRSFormMeta>) {
await AxiosPost({ AxiosPost({
title: 'New RSForm', title: 'New RSForm',
endpoint: `${config.url.BASE}rsforms/create-detailed/`, 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) { export function getRSFormDetails(target: string, request: FrontPull<IRSFormData>) {
await AxiosGet<IRSForm>({ AxiosGet({
title: `RSForm details for id=${target}`, title: `RSForm details for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/details/`, endpoint: `${config.url.BASE}rsforms/${target}/details/`,
request request: request
}); });
} }
export async function patchRSForm(target: string, request?: IFrontRequest) { export function patchRSForm(target: string, request: FrontExchange<IRSFormUpdateData, IRSFormMeta>) {
await AxiosPatch({ AxiosPatch({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`, endpoint: `${config.url.BASE}rsforms/${target}/`,
request request: request
}); });
} }
export async function patchConstituenta(target: string, request?: IFrontRequest) { export function deleteRSForm(target: string, request: FrontAction) {
await AxiosPatch({ AxiosDelete({
title: `Constituenta id=${target}`,
endpoint: `${config.url.BASE}constituents/${target}/`,
request
});
}
export async function deleteRSForm(target: string, request?: IFrontRequest) {
await AxiosDelete({
title: `RSForm id=${target}`, title: `RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/`, endpoint: `${config.url.BASE}rsforms/${target}/`,
request request: request
}); });
} }
export async function getTRSFile(target: string, request?: IFrontRequest) { export function postClaimRSForm(target: string, request: FrontPull<IRSFormMeta>) {
await AxiosGetBlob({ AxiosPost({
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({
title: `Claim on RSForm id=${target}`, title: `Claim on RSForm id=${target}`,
endpoint: `${config.url.BASE}rsforms/${target}/claim/`, endpoint: `${config.url.BASE}rsforms/${target}/claim/`,
request request: request
}); });
} }
export async function postCheckExpression(schema: string, request?: IFrontRequest) { export function getTRSFile(target: string, request: FrontPull<Blob>) {
await AxiosPost({ AxiosGet({
title: `Check expression for RSForm id=${schema}: ${request?.data.expression as string}`, title: `RSForm TRS file for id=${target}`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`, endpoint: `${config.url.BASE}rsforms/${target}/export-trs/`,
request request: request,
options: { responseType: 'blob' }
}); });
} }
export async function postNewConstituenta(schema: string, request?: IFrontRequest) { export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
await AxiosPost({ AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data.alias as string}`, title: `New Constituenta for RSForm id=${schema}: ${request.data.alias}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`,
request request: request
}); });
} }
export async function patchDeleteConstituenta(schema: string, request?: IFrontRequest) { export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
await AxiosPatch<IRSForm>({ AxiosPatch({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions title: `Delete Constituents for RSForm id=${schema}: ${request.data.items.map(item => String(item.id)).join(' ')}`,
title: `Delete Constituents for RSForm id=${schema}: ${request?.data.items.toString()}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
request request: request
}); });
} }
export async function patchMoveConstituenta(schema: string, request?: IFrontRequest) { export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
await AxiosPatch<IRSForm>({ AxiosPatch({
title: `Moving Constituents for RSForm id=${schema}: ${JSON.stringify(request?.data.items)} to ${request?.data.move_to as number}`, 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/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-moveto/`,
request request: request
}); });
} }
// ====== Helper functions =========== export function postCheckExpression(schema: string, request: FrontExchange<RSExpression, ExpressionParse>) {
async function AxiosGet<ReturnType>({ endpoint, request, title }: IAxiosRequest) { AxiosPost({
if (title) console.log(`[[${title}]] requested`); title: `Check expression for RSForm id=${schema}: ${request.data.expression }`,
if (request?.setLoading) request?.setLoading(true); endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
axios.get<ReturnType>(endpoint) 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) => { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.onSuccess) request.onSuccess(response); if (request.onSuccess) request.onSuccess(response.data);
}) })
.catch((error) => { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.showError) toast.error(error.message); if (request.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
async function AxiosGetBlob({ endpoint, request, title }: IAxiosRequest) { function AxiosPost<RequestData, ResponseData>(
if (title) console.log(`[[${title}]] requested`); { endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
if (request?.setLoading) request?.setLoading(true); ) {
axios.get(endpoint, { responseType: 'blob' }) console.log(`[[${title}]] posted`);
if (request.setLoading) request.setLoading(true);
axios.post<ResponseData>(endpoint, request.data, options)
.then((response) => { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.onSuccess) request.onSuccess(response); if (request.onSuccess) request.onSuccess(response.data);
}) })
.catch((error) => { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.showError) toast.error(error.message); if (request.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
async function AxiosPost({ endpoint, request, title }: IAxiosRequest) { function AxiosDelete<RequestData, ResponseData>(
if (title) console.log(`[[${title}]] posted`); { endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
if (request?.setLoading) request?.setLoading(true); ) {
axios.post(endpoint, request?.data) console.log(`[[${title}]] is being deleted`);
if (request.setLoading) request.setLoading(true);
axios.delete<ResponseData>(endpoint, options)
.then((response) => { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.onSuccess) request.onSuccess(response); if (request.onSuccess) request.onSuccess(response.data);
}) })
.catch((error) => { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.showError) toast.error(error.message); if (request.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request.onError) request.onError(error);
}); });
} }
async function AxiosDelete({ endpoint, request, title }: IAxiosRequest) { function AxiosPatch<RequestData, ResponseData>(
if (title) console.log(`[[${title}]] is being deleted`); { endpoint, request, title, options }: IAxiosRequest<RequestData, ResponseData>
if (request?.setLoading) request?.setLoading(true); ) {
axios.delete(endpoint) console.log(`[[${title}]] is being patrially updated`);
if (request.setLoading) request.setLoading(true);
axios.patch<ResponseData>(endpoint, request.data, options)
.then((response) => { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.onSuccess) request.onSuccess(response); 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);
});
}
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);
return response.data; return response.data;
}) })
.catch((error) => { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request.setLoading) request.setLoading(false);
if (request?.showError) toast.error(error.message); if (request.showError) toast.error(error.message);
if (request?.onError) request.onError(error); 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 // ========= Users ===========
export interface ICurrentUser { export interface IUser {
id: number id: number | null
username: string username: string
is_staff: boolean is_staff: boolean
}
// User profile data
export interface IUserProfile {
id: number
username: string
email: string email: string
first_name: string first_name: string
last_name: string last_name: string
} }
// User base info export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {}
export interface IUserInfo {
id: number export interface IUserLoginData extends Pick<IUser, 'username'> {
username: string password: string
first_name: string
last_name: string
} }
// User data for signup export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
export interface IUserSignupData {
username: string
email: string
first_name: string
last_name: string
password: string password: string
password2: string password2: string
} }
export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
export interface IUserInfo extends Omit<IUserProfile, 'email'> {}
// User data for signup // ======== Parsing ============
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'
}
// ValueClass // ValueClass
export enum ValueClass { export enum ValueClass {
INVALID = 'invalid', INVALID = 'invalid',
@ -72,25 +43,56 @@ export enum ParsingStatus {
INCORRECT = 'incorrect' 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 { export interface IConstituenta {
id: number id: number
alias: string alias: string
cstType: CstType cstType: CstType
convention?: string convention: string
term?: { term: {
raw: string raw: string
resolved?: string resolved: string
forms?: string[] forms: string[]
} }
definition?: { definition: {
formal: string formal: string
text: { text: {
raw: string raw: string
resolved?: string resolved: string
} }
} }
parse?: { parse: {
status: ParsingStatus status: ParsingStatus
valueClass: ValueClass valueClass: ValueClass
typification: string 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 { export interface IRSFormStats {
count_all: number count_all: number
count_errors: number count_errors: number
@ -117,7 +154,6 @@ export interface IRSFormStats {
count_theorem: number count_theorem: number
} }
// RSForm data
export interface IRSForm { export interface IRSForm {
id: number id: number
title: string title: string
@ -126,125 +162,24 @@ export interface IRSForm {
is_common: boolean is_common: boolean
time_create: string time_create: string
time_update: string time_update: string
owner?: number owner: number | null
items?: IConstituenta[] items: IConstituenta[]
stats?: IRSFormStats stats: IRSFormStats
} }
// RSForm user input export interface IRSFormData extends Omit<IRSForm, 'stats' > {}
export interface IRSFormCreateData { export interface IRSFormMeta extends Omit<IRSForm, 'items' | 'stats'> {}
title: string
alias: string export interface IRSFormUpdateData
comment: string extends Omit<IRSFormMeta, 'time_create' | 'time_update' | 'id' | 'owner'> {}
is_common: boolean
export interface IRSFormCreateData
extends IRSFormUpdateData {
file?: File file?: File
fileName?: string
} }
//! RS language token types enumeration // ================ Misc types ================
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,
};
// Constituenta edit mode // Constituenta edit mode
export enum EditMode { export enum EditMode {
TEXT = 'text', TEXT = 'text',
@ -280,9 +215,10 @@ export function inferStatus(parse?: ParsingStatus, value?: ValueClass): Expressi
return ExpressionStatus.VERIFIED return ExpressionStatus.VERIFIED
} }
export function CalculateStats(schema: IRSForm) { export function LoadRSFormData(schema: IRSFormData): IRSForm {
if (!schema.items) { const result = schema as IRSForm
schema.stats = { if (!result.items) {
result.stats = {
count_all: 0, count_all: 0,
count_errors: 0, count_errors: 0,
count_property: 0, count_property: 0,
@ -299,22 +235,22 @@ export function CalculateStats(schema: IRSForm) {
count_predicate: 0, count_predicate: 0,
count_theorem: 0 count_theorem: 0
} }
return; return result;
} }
schema.stats = { result.stats = {
count_all: schema.items?.length || 0, count_all: schema.items.length || 0,
count_errors: schema.items?.reduce( count_errors: schema.items.reduce(
(sum, cst) => sum + (cst.parse?.status === ParsingStatus.INCORRECT ? 1 : 0) || 0, 0), (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), (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 + (sum, cst) => sum +
((cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID) ? 1 : 0) || 0, 0), ((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), (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), (sum, cst) => sum + (cst.cstType === CstType.BASE ? 1 : 0), 0),
count_constant: schema.items?.reduce( count_constant: schema.items?.reduce(
(sum, cst) => sum + (cst.cstType === CstType.CONSTANT ? 1 : 0), 0), (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), (sum, cst) => sum + (cst.cstType === CstType.STRUCTURED ? 1 : 0), 0),
count_axiom: schema.items?.reduce( count_axiom: schema.items?.reduce(
(sum, cst) => sum + (cst.cstType === CstType.AXIOM ? 1 : 0), 0), (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), (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), (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), (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) (sum, cst) => sum + (cst.cstType === CstType.THEOREM ? 1 : 0), 0)
} }
return result;
} }
export function matchConstituenta(query: string, target?: IConstituenta) { export function matchConstituenta(query: string, target?: IConstituenta) {

View File

@ -1,7 +1,8 @@
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { type BackendCallback } from './backendAPI'; import { type DataCallback } from './backendAPI';
import { IRSFormMeta } from './models';
export function shareCurrentURLProc() { export function shareCurrentURLProc() {
const url = window.location.href + '&share'; const url = window.location.href + '&share';
@ -11,7 +12,7 @@ export function shareCurrentURLProc() {
} }
export function claimOwnershipProc( export function claimOwnershipProc(
claim: (callback: BackendCallback) => void claim: (callback: DataCallback<IRSFormMeta>) => void
) { ) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) { if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return; return;
@ -20,7 +21,7 @@ export function claimOwnershipProc(
} }
export function deleteRSFormProc( export function deleteRSFormProc(
destroy: (callback: BackendCallback) => void, destroy: (callback: DataCallback) => void,
navigate: (path: string) => void navigate: (path: string) => void
) { ) {
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) { if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
@ -33,14 +34,14 @@ export function deleteRSFormProc(
} }
export function downloadRSFormProc( export function downloadRSFormProc(
download: (callback: BackendCallback) => void, download: (callback: DataCallback<Blob>) => void,
fileName: string fileName: string
) { ) {
download((response) => { download((data) => {
try { try {
fileDownload(response.data, fileName); fileDownload(data, fileName);
} catch (error: any) { } catch (error) {
toast.error(error.message); 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 { export interface IRSButtonData {
text: string 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( export const CstTypeSelector = (Object.values(CstType)).map(
(typeStr) => { (typeStr) => {
const type = typeStr as CstType; const type = typeStr as CstType;
@ -272,3 +287,30 @@ export function createAliasFor(type: CstType, schema: IRSForm): string {
}, 1); }, 1);
return `${prefix}${index}`; 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} */ /** @type {import('tailwindcss').Config} */
module.exports = { export default {
darkMode: 'class', darkMode: 'class',
content: [ content: [
'./src/**/*.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}',

View File

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