mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Merge branch 'main' of https://github.com/IRBorisov/ConceptPortal
# Conflicts: # rsconcept/frontend/src/App.tsx
This commit is contained in:
commit
1f06f01645
|
@ -59,4 +59,5 @@ bower_components
|
||||||
|
|
||||||
|
|
||||||
# Specific items
|
# Specific items
|
||||||
docker-compose.yml
|
docker-compose-dev.yml
|
||||||
|
docker-compose-prod.yml
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
# SECURITY SENSITIVE FILES
|
# SECURITY SENSITIVE FILES
|
||||||
# persistent/*
|
secrets/
|
||||||
|
cert/
|
||||||
|
|
||||||
# External distributions
|
# External distributions
|
||||||
rsconcept/backend/import/*.whl
|
rsconcept/backend/import/*.whl
|
||||||
|
|
22
.vscode/launch.json
vendored
22
.vscode/launch.json
vendored
|
@ -52,6 +52,26 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
||||||
"args": ["-freshStart"]
|
"args": ["-freshStart"]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"name": "FE-Debug",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "${workspaceFolder}/rsconcept/frontend/node_modules/.bin/jest",
|
||||||
|
"args": [
|
||||||
|
"${fileBasenameNoExtension}",
|
||||||
|
"--runInBand",
|
||||||
|
"--watch",
|
||||||
|
"--coverage=false",
|
||||||
|
"--no-cache"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}/rsconcept/frontend",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/rsconcept/frontend/node_modules/jest/bin/jest"
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -9,6 +9,8 @@ This readme file is used mostly to document project dependencies
|
||||||
- run rsconcept\backend\LocalEnvSetup.ps1
|
- run rsconcept\backend\LocalEnvSetup.ps1
|
||||||
- run 'npm install' in rsconcept\frontend
|
- run 'npm install' in rsconcept\frontend
|
||||||
- use VSCode configs in root folder to start developement
|
- use VSCode configs in root folder to start developement
|
||||||
|
- production: create secrets secrets\db_password.txt and django_key.txt
|
||||||
|
- production: provide TLS certificate nginx\cert\portal-cert.pem and nginx\cert\portal-key.pem
|
||||||
|
|
||||||
# Frontend stack & Tooling [Vite + React + Typescript]
|
# Frontend stack & Tooling [Vite + React + Typescript]
|
||||||
<details>
|
<details>
|
||||||
|
|
19
TODO.txt
19
TODO.txt
|
@ -2,6 +2,23 @@
|
||||||
This list only contains global tech refactorings and tech debt
|
This list only contains global tech refactorings and tech debt
|
||||||
For more specific TODOs see comments in code
|
For more specific TODOs see comments in code
|
||||||
|
|
||||||
|
[Functionality]
|
||||||
|
- home page
|
||||||
|
- manuals
|
||||||
|
- текстовый модуль для разрешения отсылок
|
||||||
|
- компонент для форматирования в редакторе текста (формальное выражения + отсылки в тексте)
|
||||||
|
- блок нотификаций пользователей
|
||||||
|
- блок синтеза
|
||||||
|
- блок организации библиотеки моделей
|
||||||
|
- проектный модуль?
|
||||||
|
- обратная связь - система баг репортов
|
||||||
|
|
||||||
|
[Tech]
|
||||||
- Use migtation/fixtures to provide initial data for testing
|
- Use migtation/fixtures to provide initial data for testing
|
||||||
- USe migtation/fixtures to load example common data
|
- USe migtation/fixtures to load example common data
|
||||||
- Add HTTPS for deployment
|
|
||||||
|
[deployment]
|
||||||
|
- HTTPS
|
||||||
|
- database backup daemon
|
||||||
|
- logs collection
|
||||||
|
- status dashboard for servers
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_volume:
|
postgres_volume:
|
||||||
name: "postgres-db"
|
name: "postgresql-db"
|
||||||
django_static_volume:
|
django_static_volume:
|
||||||
name: "static"
|
name: "static"
|
||||||
django_media_volume:
|
django_media_volume:
|
||||||
|
@ -12,6 +10,12 @@ networks:
|
||||||
default:
|
default:
|
||||||
name: concept-api-net
|
name: concept-api-net
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
django_key:
|
||||||
|
file: ./secrets/django_key.txt
|
||||||
|
db_password:
|
||||||
|
file: ./secrets/db_password.txt
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -19,8 +23,8 @@ services:
|
||||||
- backend
|
- backend
|
||||||
build:
|
build:
|
||||||
context: ./rsconcept/frontend
|
context: ./rsconcept/frontend
|
||||||
ports:
|
expose:
|
||||||
- 3000:3000
|
- 3000
|
||||||
command: serve -s /home/node -l 3000
|
command: serve -s /home/node -l 3000
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,12 +32,17 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgresql-db
|
- postgresql-db
|
||||||
- nginx
|
secrets:
|
||||||
|
- db_password
|
||||||
|
- django_key
|
||||||
build:
|
build:
|
||||||
context: ./rsconcept/backend
|
context: ./rsconcept/backend
|
||||||
env_file: ./rsconcept/backend/.env.dev
|
env_file: ./rsconcept/backend/.env.prod
|
||||||
ports:
|
environment:
|
||||||
- 8000:8000
|
SECRET_KEY: /run/secrets/django_key
|
||||||
|
DB_PASSWORD: /run/secrets/db_password
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
volumes:
|
volumes:
|
||||||
- django_static_volume:/home/app/web/static
|
- django_static_volume:/home/app/web/static
|
||||||
- django_media_volume:/home/app/web/media
|
- django_media_volume:/home/app/web/media
|
||||||
|
@ -44,7 +53,11 @@ services:
|
||||||
postgresql-db:
|
postgresql-db:
|
||||||
restart: always
|
restart: always
|
||||||
image: postgres:alpine
|
image: postgres:alpine
|
||||||
env_file: ./postgresql/.env.dev
|
secrets:
|
||||||
|
- db_password
|
||||||
|
env_file: ./postgresql/.env.prod
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: /run/secrets/db_password
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_volume:/var/lib/postgresql/data
|
- postgres_volume:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
@ -54,9 +67,11 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
ports:
|
ports:
|
||||||
- 1337:80
|
- 8000:8000
|
||||||
|
- 3000:3000
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
|
||||||
volumes:
|
volumes:
|
||||||
- django_static_volume:/var/www/static
|
- django_static_volume:/var/www/static
|
||||||
- django_media_volume:/var/www/media
|
- django_media_volume:/var/www/media
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
FROM nginx:stable-alpine3.17-slim
|
FROM nginx:stable-alpine3.17-slim
|
||||||
|
|
||||||
# Сopу nginx configuration to the proxy-server
|
# Сopу nginx configuration to the proxy-server
|
||||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY ./cert/* /etc/ssl/private/
|
|
@ -1,12 +1,17 @@
|
||||||
upstream innerdjango {
|
upstream innerdjango {
|
||||||
server backend:8000;
|
server backend:8000;
|
||||||
# `backend` is the service's name in docker-compose.yml,
|
}
|
||||||
# The `innerdjango` is the name of upstream, used by nginx below.
|
|
||||||
|
upstream innerreact {
|
||||||
|
server frontend:3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8000 ssl;
|
||||||
server_name rs.acconcept.ru;
|
ssl_certificate /etc/ssl/private/portal-cert.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/portal-key.pem;
|
||||||
|
server_name dev.concept.ru www.dev.concept.ru api.portal.acconcept.ru www.api.portal.acconcept.ru;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
@ -19,4 +24,18 @@ server {
|
||||||
location /media/ {
|
location /media/ {
|
||||||
alias /var/www/media/;
|
alias /var/www/media/;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3000 ssl;
|
||||||
|
ssl_certificate /etc/ssl/private/portal-cert.pem;
|
||||||
|
ssl_certificate_key /etc/ssl/private/portal-key.pem;
|
||||||
|
server_name dev.concept.ru www.dev.concept.ru portal.acconcept.ru www.portal.acconcept.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_pass http://innerreact;
|
||||||
|
proxy_redirect default;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
POSTGRES_USER=dev-test-user
|
|
||||||
POSTGRES_PASSWORD=02BD82EE0D
|
|
||||||
POSTGRES_DB=dev-db
|
|
2
postgresql/.env.prod
Normal file
2
postgresql/.env.prod
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
POSTGRES_USER=portal-admin
|
||||||
|
POSTGRES_DB=portal-db
|
|
@ -46,7 +46,7 @@ function AddAdmin {
|
||||||
$env:DJANGO_SUPERUSER_USERNAME = 'admin'
|
$env:DJANGO_SUPERUSER_USERNAME = 'admin'
|
||||||
$env:DJANGO_SUPERUSER_PASSWORD = '1234'
|
$env:DJANGO_SUPERUSER_PASSWORD = '1234'
|
||||||
$env:DJANGO_SUPERUSER_EMAIL = 'admin@admin.com'
|
$env:DJANGO_SUPERUSER_EMAIL = 'admin@admin.com'
|
||||||
& $pyExec $djangoSrc createsuperuser --noinput
|
& $pyExec $djangoSrc createsuperuser --noinput
|
||||||
}
|
}
|
||||||
|
|
||||||
function DoMigrations {
|
function DoMigrations {
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
# Application settings
|
|
||||||
SECRET_KEY=django-insecure-)rq@!&v7l2r%2%q#n!uq+zk@=&yc0^&ql^7%2!%9u)vt1x&j=d
|
|
||||||
ALLOWED_HOSTS=rs.acconcept.ru;localhost
|
|
||||||
CSRF_TRUSTED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000
|
|
||||||
CORS_ALLOWED_ORIGINS=http://rs.acconcept.ru:3000;localhost:3000
|
|
||||||
|
|
||||||
|
|
||||||
# File locations
|
|
||||||
STATIC_ROOT=/home/app/web/static
|
|
||||||
MEDIA_ROOT=/home/app/web/media
|
|
||||||
|
|
||||||
|
|
||||||
# Database settings
|
|
||||||
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
|
||||||
DB_NAME=dev-db
|
|
||||||
DB_USER=dev-test-user
|
|
||||||
DB_PASSWORD=02BD82EE0D
|
|
||||||
DB_HOST=postgresql-db
|
|
||||||
DB_PORT=5432
|
|
||||||
|
|
||||||
|
|
||||||
# Debug settings
|
|
||||||
DEBUG=1
|
|
||||||
PYTHONDEVMODE=1
|
|
||||||
PYTHONTRACEMALLOC=1
|
|
24
rsconcept/backend/.env.prod
Normal file
24
rsconcept/backend/.env.prod
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Application settings
|
||||||
|
|
||||||
|
ALLOWED_HOSTS=localhost;portal.acconcept.ru;dev.concept.ru
|
||||||
|
CSRF_TRUSTED_ORIGINS=https://dev.concept.ru:3000;https://localhost:3000;https://portal.acconcept.ru:3000
|
||||||
|
CORS_ALLOWED_ORIGINS=https://dev.concept.ru:3000;https://localhost:3000;https://portal.acconcept.ru:3000
|
||||||
|
|
||||||
|
|
||||||
|
# File locations
|
||||||
|
STATIC_ROOT=/home/app/web/static
|
||||||
|
MEDIA_ROOT=/home/app/web/media
|
||||||
|
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
DB_NAME=portal-db
|
||||||
|
DB_USER=portal-admin
|
||||||
|
DB_HOST=postgresql-db
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
|
# Debug settings
|
||||||
|
DEBUG=0
|
||||||
|
PYTHONDEVMODE=0
|
||||||
|
PYTHONTRACEMALLOC=0
|
|
@ -55,8 +55,9 @@ RUN pip install --no-cache /wheels/* && \
|
||||||
rm -rf /wheels
|
rm -rf /wheels
|
||||||
|
|
||||||
# Copy application sources and setup permissions
|
# Copy application sources and setup permissions
|
||||||
COPY apps/ ./apps/
|
COPY apps/ ./apps
|
||||||
COPY project/ ./project
|
COPY project/ ./project
|
||||||
|
COPY data/ ./data
|
||||||
COPY manage.py entrypoint.sh ./
|
COPY manage.py entrypoint.sh ./
|
||||||
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
|
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
|
||||||
chmod +x $APP_HOME/entrypoint.sh && \
|
chmod +x $APP_HOME/entrypoint.sh && \
|
||||||
|
@ -64,6 +65,8 @@ RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.sh && \
|
||||||
chmod -R a+rwx $APP_HOME/static && \
|
chmod -R a+rwx $APP_HOME/static && \
|
||||||
chmod -R a+rwx $APP_HOME/media
|
chmod -R a+rwx $APP_HOME/media
|
||||||
|
|
||||||
|
RUN
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
WORKDIR $APP_HOME
|
WORKDIR $APP_HOME
|
||||||
|
|
||||||
|
|
|
@ -43,9 +43,15 @@ class ConstituentaSerializer(serializers.ModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
read_only_fields = ('id', 'order', 'alias', 'cst_type')
|
read_only_fields = ('id', 'order', 'alias', 'cst_type')
|
||||||
|
|
||||||
def update(self, instance: Constituenta, validated_data):
|
def update(self, instance: Constituenta, validated_data) -> Constituenta:
|
||||||
|
if ('term_raw' in validated_data):
|
||||||
|
validated_data['term_resolved'] = validated_data['term_raw']
|
||||||
|
if ('definition_raw' in validated_data):
|
||||||
|
validated_data['definition_resolved'] = validated_data['definition_raw']
|
||||||
|
|
||||||
|
result = super().update(instance, validated_data)
|
||||||
instance.schema.save()
|
instance.schema.save()
|
||||||
return super().update(instance, validated_data)
|
return result
|
||||||
|
|
||||||
|
|
||||||
class StandaloneCstSerializer(serializers.ModelSerializer):
|
class StandaloneCstSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
@ -34,6 +34,7 @@ class TestIntegrations(TestCase):
|
||||||
schema = self._default_schema()
|
schema = self._default_schema()
|
||||||
out1 = json.loads(pc.check_expression(schema, 'X1=X1'))
|
out1 = json.loads(pc.check_expression(schema, 'X1=X1'))
|
||||||
self.assertTrue(out1['parseResult'])
|
self.assertTrue(out1['parseResult'])
|
||||||
|
self.assertEqual(len(out1['args']), 0)
|
||||||
|
|
||||||
out2 = json.loads(pc.check_expression(schema, 'X1=X2'))
|
out2 = json.loads(pc.check_expression(schema, 'X1=X2'))
|
||||||
self.assertFalse(out2['parseResult'])
|
self.assertFalse(out2['parseResult'])
|
||||||
|
|
|
@ -31,6 +31,10 @@ class TestConstituentaAPI(APITestCase):
|
||||||
alias='X1', schema=self.rsform_owned, order=1, convention='Test')
|
alias='X1', schema=self.rsform_owned, order=1, convention='Test')
|
||||||
self.cst2 = Constituenta.objects.create(
|
self.cst2 = Constituenta.objects.create(
|
||||||
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1')
|
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1')
|
||||||
|
self.cst3 = Constituenta.objects.create(
|
||||||
|
alias='X3', schema=self.rsform_owned, order=2,
|
||||||
|
term_raw='Test1', term_resolved='Test1',
|
||||||
|
definition_raw='Test1', definition_resolved='Test2')
|
||||||
|
|
||||||
def test_retrieve(self):
|
def test_retrieve(self):
|
||||||
response = self.client.get(f'/api/constituents/{self.cst1.id}/')
|
response = self.client.get(f'/api/constituents/{self.cst1.id}/')
|
||||||
|
@ -57,6 +61,17 @@ class TestConstituentaAPI(APITestCase):
|
||||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_partial_update_update_resolved(self):
|
||||||
|
data = json.dumps({
|
||||||
|
'term_raw': 'New term',
|
||||||
|
'definition_raw': 'New def'
|
||||||
|
})
|
||||||
|
response = self.client.patch(f'/api/constituents/{self.cst3.id}/', data, content_type='application/json')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.cst3.refresh_from_db()
|
||||||
|
self.assertEqual(self.cst3.term_resolved, 'New term')
|
||||||
|
self.assertEqual(self.cst3.definition_resolved, 'New def')
|
||||||
|
|
||||||
def test_readonly_cst_fields(self):
|
def test_readonly_cst_fields(self):
|
||||||
data = json.dumps({'alias': 'X33', 'order': 10})
|
data = json.dumps({'alias': 'X33', 'order': 10})
|
||||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||||
|
|
|
@ -11,6 +11,7 @@ then
|
||||||
echo "Ready!"
|
echo "Ready!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd $APP_HOME
|
||||||
python $APP_HOME/manage.py collectstatic --noinput --clear
|
python $APP_HOME/manage.py collectstatic --noinput --clear
|
||||||
python $APP_HOME/manage.py migrate
|
python $APP_HOME/manage.py migrate
|
||||||
|
|
||||||
|
|
|
@ -82,9 +82,9 @@ MIDDLEWARE = [
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'project.urls'
|
ROOT_URLCONF = 'project.urls'
|
||||||
LOGIN_URL = '/accounts/login/'
|
LOGIN_URL = '/admin/login/'
|
||||||
LOGIN_REDIRECT_URL = '/home'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
LOGOUT_REDIRECT_URL = '/home'
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
|
24
rsconcept/frontend/.gitignore
vendored
24
rsconcept/frontend/.gitignore
vendored
|
@ -1,24 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
|
@ -11,6 +11,7 @@ WORKDIR /result
|
||||||
|
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
ENV NODE_ENV production
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ========= Server =======
|
# ========= Server =======
|
||||||
|
|
4
rsconcept/frontend/package-lock.json
generated
4
rsconcept/frontend/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
|
@ -16,9 +17,21 @@ import RSFormPage from './pages/RSFormPage';
|
||||||
import UserProfilePage from './pages/UserProfilePage';
|
import UserProfilePage from './pages/UserProfilePage';
|
||||||
|
|
||||||
function App () {
|
function App () {
|
||||||
const {noNavigation} = useConceptTheme()
|
const { noNavigation } = useConceptTheme();
|
||||||
const nav_height: string = noNavigation ? "7.5rem" : "7.4rem";
|
|
||||||
const main_clsN: string = `min-h-[calc(100vh-${nav_height})] px-2 h-fit`;
|
const scrollWindowSize = useMemo(
|
||||||
|
() => {
|
||||||
|
return !noNavigation ?
|
||||||
|
'max-h-[calc(100vh-4.5rem)]'
|
||||||
|
: 'max-h-[100vh]';
|
||||||
|
}, [noNavigation]);
|
||||||
|
const mainSize = useMemo(
|
||||||
|
() => {
|
||||||
|
return !noNavigation ?
|
||||||
|
'min-h-[calc(100vh-12rem)]'
|
||||||
|
: 'min-h-[calc(100vh-8rem)] ';
|
||||||
|
}, [noNavigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='antialiased clr-app'>
|
<div className='antialiased clr-app'>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
@ -28,7 +41,9 @@ function App () {
|
||||||
draggable={false}
|
draggable={false}
|
||||||
pauseOnFocusLoss={false}
|
pauseOnFocusLoss={false}
|
||||||
/>
|
/>
|
||||||
<main className={main_clsN}>
|
|
||||||
|
<div className={`${scrollWindowSize} overflow-auto`}>
|
||||||
|
<main className={`${mainSize} px-2`}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={ <HomePage/>} />
|
<Route path='/' element={ <HomePage/>} />
|
||||||
|
|
||||||
|
@ -46,6 +61,7 @@ function App () {
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,9 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
|
||||||
function Button({
|
function Button({
|
||||||
id, text, icon, tooltip,
|
id, text, icon, tooltip,
|
||||||
dense, disabled,
|
dense, disabled,
|
||||||
borderClass = 'border rounded', colorClass = 'clr-btn-default', widthClass = 'w-fit h-fit',
|
borderClass = 'border rounded',
|
||||||
|
colorClass = 'clr-btn-default',
|
||||||
|
widthClass = 'w-fit h-fit',
|
||||||
loading, onClick,
|
loading, onClick,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
|
|
|
@ -6,14 +6,15 @@ import Button from './Button';
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title?: string
|
title?: string
|
||||||
submitText?: string
|
submitText?: string
|
||||||
|
readonly?: boolean
|
||||||
canSubmit?: boolean
|
canSubmit?: boolean
|
||||||
hideWindow: () => void
|
hideWindow: () => void
|
||||||
onSubmit: () => void
|
onSubmit?: () => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) {
|
function Modal({ title, hideWindow, onSubmit, readonly, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
useEscapeKey(hideWindow);
|
useEscapeKey(hideWindow);
|
||||||
|
|
||||||
|
@ -24,29 +25,32 @@ function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, sub
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
hideWindow();
|
hideWindow();
|
||||||
onSubmit();
|
if (onSubmit) onSubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
||||||
</div>
|
</div>
|
||||||
<div ref={ref} className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit h-fit z-[60] clr-card border shadow-md mb-[5rem]'>
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-6 py-4 flex flex-col w-fit max-w-[95vw] h-fit z-[60] clr-card border shadow-md mb-[5rem]'
|
||||||
|
>
|
||||||
{ title && <h1 className='mb-2 text-xl font-bold text-center'>{title}</h1> }
|
{ title && <h1 className='mb-2 text-xl font-bold text-center'>{title}</h1> }
|
||||||
<div>
|
<div className='max-h-[calc(95vh-15rem)] overflow-auto'>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between w-full pt-4 mt-2 border-t-4'>
|
<div className='flex justify-center w-full gap-4 pt-4 mt-2 border-t-4'>
|
||||||
<Button
|
{!readonly && <Button
|
||||||
text={submitText}
|
text={submitText}
|
||||||
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
||||||
colorClass='clr-btn-primary'
|
colorClass='clr-btn-primary'
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>}
|
||||||
<Button
|
<Button
|
||||||
text='Отмена'
|
text={readonly ? 'Закрыть' : 'Отмена'}
|
||||||
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface SubmitButtonProps {
|
||||||
function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: SubmitButtonProps) {
|
function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: SubmitButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button type='submit'
|
<button type='submit'
|
||||||
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold disabled:cursor-not-allowed rounded clr-btn-primary ${loading ? ' cursor-progress' : ''}`}
|
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold disabled:cursor-not-allowed border rounded clr-btn-primary ${loading ? ' cursor-progress' : ''}`}
|
||||||
disabled={disabled ?? loading}
|
disabled={disabled ?? loading}
|
||||||
>
|
>
|
||||||
{icon && <span>{icon}</span>}
|
{icon && <span>{icon}</span>}
|
||||||
|
|
|
@ -1,24 +1,20 @@
|
||||||
|
import { TextareaHTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextAreaProps {
|
interface TextAreaProps
|
||||||
id: string
|
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
|
||||||
label: string
|
label: string
|
||||||
required?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
spellCheck?: boolean
|
|
||||||
placeholder?: string
|
|
||||||
widthClass?: string
|
widthClass?: string
|
||||||
rows?: number
|
colorClass?: string
|
||||||
value?: string | ReadonlyArray<string> | number | undefined;
|
|
||||||
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
|
||||||
onFocus?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextArea({
|
function TextArea({
|
||||||
id, label, placeholder,
|
id, label, required,
|
||||||
required, spellCheck, disabled,
|
widthClass = 'w-full',
|
||||||
widthClass = 'w-full', rows = 4, value,
|
colorClass = 'colorClass',
|
||||||
onChange, onFocus
|
rows = 4,
|
||||||
|
...props
|
||||||
}: TextAreaProps) {
|
}: TextAreaProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'>
|
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'>
|
||||||
|
@ -28,15 +24,10 @@ function TextArea({
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
/>
|
/>
|
||||||
<textarea id={id}
|
<textarea id={id}
|
||||||
className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 ' + widthClass}
|
className={`px-3 py-2 mt-2 leading-tight border shadow ${colorClass} ${widthClass}`}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
placeholder={placeholder}
|
|
||||||
required={required}
|
required={required}
|
||||||
value={value}
|
{...props}
|
||||||
onChange={onChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
disabled={disabled}
|
|
||||||
spellCheck={spellCheck}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,14 +3,17 @@ import { type InputHTMLAttributes } from 'react';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextInputProps
|
interface TextInputProps
|
||||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
widthClass?: string
|
widthClass?: string
|
||||||
|
colorClass?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextInput({
|
function TextInput({
|
||||||
id, required, label, widthClass = 'w-full',
|
id, required, label,
|
||||||
|
widthClass = 'w-full',
|
||||||
|
colorClass = 'clr-input',
|
||||||
...props
|
...props
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -21,7 +24,7 @@ function TextInput({
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
/>
|
/>
|
||||||
<input id={id}
|
<input id={id}
|
||||||
className={'px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800 truncate hover:text-clip ' + widthClass}
|
className={`px-3 py-2 mt-2 leading-tight border shadow truncate hover:text-clip ${colorClass} ${widthClass}`}
|
||||||
required={required}
|
required={required}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { urls } from '../utils/constants';
|
import { urls } from '../utils/constants';
|
||||||
|
import { GithubIcon } from './Icons';
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className='z-50 px-4 pt-2 pb-4 border-t-2 t clr-footer'>
|
<footer className='z-50 px-4 pt-2 pb-4 border-t-2 clr-footer'>
|
||||||
<div className='flex items-stretch justify-center w-full mx-auto'>
|
<div className='flex items-stretch justify-center w-full mx-auto'>
|
||||||
<div className='px-4 underline'>
|
<div className='px-4 underline whitespace-nowrap'>
|
||||||
<Link to='/manuals' tabIndex={-1}>Справка</Link> <br/>
|
<Link to='/manuals' tabIndex={-1}>Справка</Link> <br/>
|
||||||
<Link to='/library?filter=common' tabIndex={-1}>Библиотека КС</Link> <br/>
|
<Link to='/library?filter=common' tabIndex={-1}>Библиотека КС</Link> <br/>
|
||||||
|
<a href={urls.gitrepo} className='flex'>
|
||||||
|
<GithubIcon />
|
||||||
|
<span className='ml-1'>Репозиторий</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className='px-4 underline border-gray-400 border-x dark:border-gray-300'>
|
<div className='px-4 underline border-gray-400 border-x dark:border-gray-300'>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -23,9 +28,9 @@ function Footer() {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className='max-w-xl px-4 text-sm'>
|
<div className='max-w-[28rem] px-4 text-sm'>
|
||||||
<p className='mt-0.5'>© 2023 ЦИВТ КОНЦЕПТ</p>
|
<p className='mt-0.5'>© 2023 ЦИВТ КОНЦЕПТ</p>
|
||||||
<p>Данный инструмент работы с экспликациями концептуальных схем в родоструктурной форме является уникальной Российской разработкой и вобрал в себя разработки начиная с 1990-х годов</p>
|
<p>Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с помощью математического аппарата родов структур</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer >
|
</footer >
|
||||||
|
|
|
@ -292,3 +292,11 @@ export function HelpIcon(props: IconProps) {
|
||||||
</IconSVG>
|
</IconSVG>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GithubIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M12 2.247a10 10 0 00-3.162 19.487c.5.088.687-.212.687-.475 0-.237-.012-1.025-.012-1.862-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 00-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 011.538 1.025 2.137 2.137 0 002.912.825 2.104 2.104 0 01.638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 011.025-2.688 3.594 3.594 0 01.1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 015 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 01.1 2.65 3.869 3.869 0 011.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 01.675 1.85c0 1.338-.012 2.413-.012 2.75 0 .263.187.575.687.475A10.005 10.005 0 0012 2.247z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
interface InfoMessageProps {
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfoMessage({ message }: InfoMessageProps) {
|
|
||||||
return (
|
|
||||||
<p className='font-bold'>{ message }</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InfoMessage;
|
|
|
@ -18,7 +18,7 @@ function Navigation () {
|
||||||
const navigateHelp = () => { navigate('/manuals') };
|
const navigateHelp = () => { navigate('/manuals') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='sticky top-0 left-0 right-0 z-50'>
|
<nav className='sticky top-0 left-0 right-0 z-50 h-fit'>
|
||||||
{!noNavigation &&
|
{!noNavigation &&
|
||||||
<button
|
<button
|
||||||
title='Скрыть навигацию'
|
title='Скрыть навигацию'
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons';
|
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons';
|
||||||
import NavigationButton from './NavigationButton';
|
import NavigationButton from './NavigationButton';
|
||||||
|
|
||||||
function UserTools() {
|
function UserTools() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const navigateCreateRSForm = () => { navigate('/rsform-create'); };
|
const navigateCreateRSForm = () => { navigate('/rsform-create'); };
|
||||||
const navigateMyWork = () => { navigate('/library?filter=personal'); };
|
const navigateMyWork = () => { navigate('/library?filter=personal'); };
|
||||||
|
@ -24,7 +26,7 @@ function UserTools() {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} />
|
<NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} />
|
||||||
<NavigationButton icon={<BellIcon />} description='Уведомления' onClick={handleNotifications} />
|
{ user && user.is_staff && <NavigationButton icon={<BellIcon />} description='Уведомления' onClick={handleNotifications} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import TextURL from './Common/TextURL';
|
import TextURL from './Common/TextURL';
|
||||||
import InfoMessage from './InfoMessage';
|
|
||||||
|
|
||||||
interface RequireAuthProps {
|
interface RequireAuthProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -13,12 +12,12 @@ function RequireAuth({ children }: RequireAuthProps) {
|
||||||
<>
|
<>
|
||||||
{user && children}
|
{user && children}
|
||||||
{!user &&
|
{!user &&
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center mt-2 gap-1'>
|
||||||
<InfoMessage message={'Данная функция доступна только зарегистрированным пользователям. Пожалуйста войдите в систему'} />
|
<p><b>Данная страница доступна только зарегистрированным пользователям</b></p>
|
||||||
<div className='flex flex-col items-start'>
|
<p className='mb-2'>Пожалуйста войдите в систему</p>
|
||||||
<TextURL text='Войти в систему...' href='login' />
|
<TextURL text='Войти в систему' href='/login'/>
|
||||||
<TextURL text='Зарегистрироваться...' href='signup' />
|
<TextURL text='Зарегистрироваться' href='/signup'/>
|
||||||
</div>
|
<TextURL text='Начальная страница' href='/'/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface IAuthContext {
|
||||||
login: (data: IUserLoginData, callback?: DataCallback) => void
|
login: (data: IUserLoginData, callback?: DataCallback) => void
|
||||||
logout: (callback?: DataCallback) => void
|
logout: (callback?: DataCallback) => void
|
||||||
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
|
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
|
||||||
|
reload: (callback?: () => void) => void
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: ErrorInfo
|
error: ErrorInfo
|
||||||
setError: (error: ErrorInfo) => void
|
setError: (error: ErrorInfo) => void
|
||||||
|
@ -36,20 +37,19 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||||
|
|
||||||
const reload = useCallback(
|
const reload = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
getAuth({
|
getAuth({
|
||||||
onError: () => { setUser(undefined); },
|
onError: () => { setUser(undefined); },
|
||||||
onSuccess: currentUser => {
|
onSuccess: currentUser => {
|
||||||
if (currentUser.id) {
|
if (currentUser.id) {
|
||||||
setUser(currentUser);
|
setUser(currentUser);
|
||||||
} else {
|
} else {
|
||||||
setUser(undefined);
|
setUser(undefined);
|
||||||
}
|
|
||||||
if (callback) callback();
|
|
||||||
}
|
}
|
||||||
});
|
if (callback) callback();
|
||||||
}, [setUser]
|
}
|
||||||
);
|
});
|
||||||
|
}, [setUser]);
|
||||||
|
|
||||||
function login(data: IUserLoginData, callback?: DataCallback) {
|
function login(data: IUserLoginData, callback?: DataCallback) {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
@ -87,7 +87,7 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{ user, login, logout, signup, loading, error, setError }}
|
value={{ user, login, logout, signup, loading, error, reload, setError }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { ErrorInfo } from '../components/BackendError';
|
import { ErrorInfo } from '../components/BackendError';
|
||||||
import { getLibrary } from '../utils/backendAPI';
|
import { DataCallback, getLibrary, postNewRSForm } from '../utils/backendAPI';
|
||||||
import { ILibraryFilter, IRSFormMeta, matchRSFormMeta } from '../utils/models';
|
import { ILibraryFilter, IRSFormCreateData, IRSFormMeta, matchRSFormMeta } from '../utils/models';
|
||||||
|
|
||||||
interface ILibraryContext {
|
interface ILibraryContext {
|
||||||
items: IRSFormMeta[]
|
items: IRSFormMeta[]
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
processing: boolean
|
||||||
error: ErrorInfo
|
error: ErrorInfo
|
||||||
setError: (error: ErrorInfo) => void
|
setError: (error: ErrorInfo) => void
|
||||||
|
|
||||||
reload: () => void
|
reload: () => void
|
||||||
filter: (params: ILibraryFilter) => IRSFormMeta[]
|
filter: (params: ILibraryFilter) => IRSFormMeta[]
|
||||||
|
createSchema: (data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const LibraryContext = createContext<ILibraryContext | null>(null)
|
const LibraryContext = createContext<ILibraryContext | null>(null)
|
||||||
|
@ -30,9 +32,10 @@ interface LibraryStateProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryState = ({ children }: LibraryStateProps) => {
|
export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const [items, setItems] = useState<IRSFormMeta[]>([])
|
const [ items, setItems ] = useState<IRSFormMeta[]>([])
|
||||||
const [loading, setLoading] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
const [ processing, setProcessing ] = useState(false);
|
||||||
|
const [ error, setError ] = useState<ErrorInfo>(undefined);
|
||||||
|
|
||||||
const filter = useCallback(
|
const filter = useCallback(
|
||||||
(params: ILibraryFilter) => {
|
(params: ILibraryFilter) => {
|
||||||
|
@ -63,12 +66,27 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload();
|
reload();
|
||||||
}, [reload])
|
}, [reload]);
|
||||||
|
|
||||||
|
const createSchema = useCallback(
|
||||||
|
(data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => {
|
||||||
|
setError(undefined);
|
||||||
|
postNewRSForm({
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => { setError(error); },
|
||||||
|
onSuccess: newSchema => {
|
||||||
|
reload();
|
||||||
|
if (callback) callback(newSchema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryContext.Provider value={{
|
<LibraryContext.Provider value={{
|
||||||
items, loading, error, setError,
|
items, loading, processing, error, setError,
|
||||||
reload, filter
|
reload, filter, createSchema
|
||||||
}}>
|
}}>
|
||||||
{ children }
|
{ children }
|
||||||
</LibraryContext.Provider>
|
</LibraryContext.Provider>
|
||||||
|
|
|
@ -5,14 +5,17 @@ import { type ErrorInfo } from '../components/BackendError'
|
||||||
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
||||||
import {
|
import {
|
||||||
type DataCallback, deleteRSForm, getTRSFile,
|
type DataCallback, deleteRSForm, getTRSFile,
|
||||||
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchResetAliases,
|
patchConstituenta, patchDeleteConstituenta,
|
||||||
patchRSForm,
|
patchMoveConstituenta, patchResetAliases, patchRSForm,
|
||||||
patchUploadTRS, postClaimRSForm, postCloneRSForm,postNewConstituenta} from '../utils/backendAPI'
|
patchUploadTRS, postClaimRSForm, postCloneRSForm, postNewConstituenta
|
||||||
|
} from '../utils/backendAPI'
|
||||||
import {
|
import {
|
||||||
IConstituentaList, IConstituentaMeta, ICstCreateData,
|
IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData, IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData,
|
||||||
|
IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||||
} from '../utils/models'
|
} from '../utils/models'
|
||||||
import { useAuth } from './AuthContext'
|
import { useAuth } from './AuthContext'
|
||||||
|
import { useLibrary } from './LibraryContext'
|
||||||
|
|
||||||
interface IRSFormContext {
|
interface IRSFormContext {
|
||||||
schema?: IRSForm
|
schema?: IRSForm
|
||||||
|
@ -63,15 +66,16 @@ interface RSFormStateProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth();
|
||||||
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID })
|
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
|
||||||
const [processing, setProcessing] = useState(false)
|
const [ processing, setProcessing ] = useState(false);
|
||||||
|
const library = useLibrary();
|
||||||
|
|
||||||
const [isForceAdmin, setIsForceAdmin] = useState(false)
|
const [ isForceAdmin, setIsForceAdmin ] = useState(false);
|
||||||
const [isReadonly, setIsReadonly] = useState(false)
|
const [ isReadonly, setIsReadonly ] = useState(false);
|
||||||
|
|
||||||
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner])
|
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner]);
|
||||||
const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner])
|
const isClaimable = useMemo(() => user?.id !== schema?.owner || false, [user, schema?.owner]);
|
||||||
const isEditable = useMemo(
|
const isEditable = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return (
|
return (
|
||||||
|
@ -83,181 +87,185 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
const isTracking = useMemo(
|
const isTracking = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return true
|
return true
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const toggleTracking = useCallback(
|
const toggleTracking = useCallback(
|
||||||
() => {
|
() => {
|
||||||
toast('not implemented yet')
|
toast.info('Отслеживание в разработке...')
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => {
|
(data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
patchRSForm(schemaID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(Object.assign(schema, newData));
|
||||||
|
if (callback) callback(newData);
|
||||||
}
|
}
|
||||||
setError(undefined)
|
});
|
||||||
patchRSForm(schemaID, {
|
}, [schemaID, setError, setSchema, schema]);
|
||||||
data: data,
|
|
||||||
showError: true,
|
|
||||||
setLoading: setProcessing,
|
|
||||||
onError: error => { setError(error) },
|
|
||||||
onSuccess: newData => {
|
|
||||||
setSchema(Object.assign(schema, newData));
|
|
||||||
if (callback) callback(newData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [schemaID, setError, setSchema, schema])
|
|
||||||
|
|
||||||
const upload = useCallback(
|
const upload = useCallback(
|
||||||
(data: IRSFormUploadData, callback?: () => void) => {
|
(data: IRSFormUploadData, callback?: () => void) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
patchUploadTRS(schemaID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(newData);
|
||||||
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
setError(undefined)
|
});
|
||||||
patchUploadTRS(schemaID, {
|
}, [schemaID, setError, setSchema, schema]);
|
||||||
data: data,
|
|
||||||
showError: true,
|
|
||||||
setLoading: setProcessing,
|
|
||||||
onError: error => { setError(error) },
|
|
||||||
onSuccess: newData => {
|
|
||||||
setSchema(newData);
|
|
||||||
if (callback) callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [schemaID, setError, setSchema, schema])
|
|
||||||
|
|
||||||
const destroy = useCallback(
|
const destroy = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
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: () => {
|
onSuccess: () => {
|
||||||
setSchema(undefined);
|
setSchema(undefined);
|
||||||
if (callback) callback();
|
library.reload();
|
||||||
}
|
if (callback) callback();
|
||||||
});
|
}
|
||||||
}, [schemaID, setError, setSchema])
|
});
|
||||||
|
}, [schemaID, setError, setSchema, library]);
|
||||||
|
|
||||||
const claim = useCallback(
|
const claim = useCallback(
|
||||||
(callback?: DataCallback<IRSFormMeta>) => {
|
(callback?: DataCallback<IRSFormMeta>) => {
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
postClaimRSForm(schemaID, {
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(Object.assign(schema, newData));
|
||||||
|
if (callback) callback(newData);
|
||||||
}
|
}
|
||||||
setError(undefined)
|
});
|
||||||
postClaimRSForm(schemaID, {
|
}, [schemaID, setError, schema, user, setSchema]);
|
||||||
showError: true,
|
|
||||||
setLoading: setProcessing,
|
|
||||||
onError: error => { setError(error) },
|
|
||||||
onSuccess: newData => {
|
|
||||||
setSchema(Object.assign(schema, newData));
|
|
||||||
if (callback) callback(newData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [schemaID, setError, schema, user, setSchema])
|
|
||||||
|
|
||||||
const clone = useCallback(
|
const clone = useCallback(
|
||||||
(data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
(data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
postCloneRSForm(schemaID, {
|
||||||
|
data: data,
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => setError(error),
|
||||||
|
onSuccess: newSchema => {
|
||||||
|
library.reload();
|
||||||
|
if (callback) callback(newSchema);
|
||||||
}
|
}
|
||||||
setError(undefined)
|
});
|
||||||
postCloneRSForm(schemaID, {
|
}, [schemaID, setError, schema, user, library]);
|
||||||
data: data,
|
|
||||||
showError: true,
|
|
||||||
setLoading: setProcessing,
|
|
||||||
onError: error => { setError(error) },
|
|
||||||
onSuccess: callback
|
|
||||||
});
|
|
||||||
}, [schemaID, setError, schema, user])
|
|
||||||
|
|
||||||
const resetAliases = useCallback(
|
const resetAliases = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
if (!schema || !user) {
|
if (!schema || !user) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
setError(undefined)
|
||||||
|
patchResetAliases(schemaID, {
|
||||||
|
showError: true,
|
||||||
|
setLoading: setProcessing,
|
||||||
|
onError: error => { setError(error) },
|
||||||
|
onSuccess: newData => {
|
||||||
|
setSchema(Object.assign(schema, newData));
|
||||||
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
setError(undefined)
|
});
|
||||||
patchResetAliases(schemaID, {
|
}, [schemaID, setError, schema, user, setSchema]);
|
||||||
showError: true,
|
|
||||||
setLoading: setProcessing,
|
|
||||||
onError: error => { setError(error) },
|
|
||||||
onSuccess: newData => {
|
|
||||||
setSchema(Object.assign(schema, newData));
|
|
||||||
if (callback) callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [schemaID, setError, schema, user, setSchema])
|
|
||||||
|
|
||||||
const download = useCallback(
|
const download = useCallback(
|
||||||
(callback: DataCallback<Blob>) => {
|
(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
|
||||||
});
|
});
|
||||||
}, [schemaID, setError])
|
}, [schemaID, setError]);
|
||||||
|
|
||||||
const cstCreate = useCallback(
|
const cstCreate = useCallback(
|
||||||
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
postNewConstituenta(schemaID, {
|
postNewConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => { setError(error) },
|
onError: error => { setError(error) },
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData.schema);
|
setSchema(newData.schema);
|
||||||
if (callback) callback(newData.new_cst);
|
if (callback) callback(newData.new_cst);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, setSchema]);
|
}, [schemaID, setError, setSchema]);
|
||||||
|
|
||||||
const cstDelete = useCallback(
|
const cstDelete = useCallback(
|
||||||
(data: IConstituentaList, callback?: () => void) => {
|
(data: IConstituentaList, callback?: () => void) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
patchDeleteConstituenta(schemaID, {
|
patchDeleteConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => { setError(error) },
|
onError: error => { setError(error) },
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, setSchema]);
|
}, [schemaID, setError, setSchema]);
|
||||||
|
|
||||||
const cstUpdate = useCallback(
|
const cstUpdate = useCallback(
|
||||||
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
|
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
patchConstituenta(String(data.id), {
|
patchConstituenta(String(data.id), {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => { setError(error) },
|
onError: error => { setError(error) },
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
reload(setProcessing, () => { if (callback != null) callback(newData); })
|
reload(setProcessing, () => { if (callback != null) callback(newData); })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [setError, reload])
|
}, [setError, reload]);
|
||||||
|
|
||||||
const cstMoveTo = useCallback(
|
const cstMoveTo = useCallback(
|
||||||
(data: ICstMovetoData, callback?: () => void) => {
|
(data: ICstMovetoData, callback?: () => void) => {
|
||||||
setError(undefined)
|
setError(undefined)
|
||||||
patchMoveConstituenta(schemaID, {
|
patchMoveConstituenta(schemaID, {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: error => { setError(error) },
|
onError: error => { setError(error) },
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setSchema(newData);
|
setSchema(newData);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [schemaID, setError, setSchema]);
|
}, [schemaID, setError, setSchema]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RSFormContext.Provider value={{
|
<RSFormContext.Provider value={{
|
||||||
|
@ -273,5 +281,5 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||||
}}>
|
}}>
|
||||||
{ children }
|
{ children }
|
||||||
</RSFormContext.Provider>
|
</RSFormContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import { type ErrorInfo } from '../components/BackendError';
|
|
||||||
import { DataCallback, postNewRSForm } from '../utils/backendAPI';
|
|
||||||
import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
|
|
||||||
|
|
||||||
function useNewRSForm() {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
|
||||||
|
|
||||||
function createSchema(data: IRSFormCreateData, onSuccess: DataCallback<IRSFormMeta>) {
|
|
||||||
setError(undefined);
|
|
||||||
postNewRSForm({
|
|
||||||
data: data,
|
|
||||||
showError: true,
|
|
||||||
setLoading: setLoading,
|
|
||||||
onError: error => { setError(error); },
|
|
||||||
onSuccess: onSuccess
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { createSchema, error, setError, loading };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useNewRSForm;
|
|
|
@ -51,6 +51,10 @@
|
||||||
@apply border-gray-400 dark:border-gray-300 bg-white dark:bg-gray-700
|
@apply border-gray-400 dark:border-gray-300 bg-white dark:bg-gray-700
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clr-input {
|
||||||
|
@apply dark:bg-black bg-white disabled:bg-[#f0f2f7] dark:disabled:bg-gray-700
|
||||||
|
}
|
||||||
|
|
||||||
.clr-footer {
|
.clr-footer {
|
||||||
@apply text-gray-600 bg-white border-gray-400 dark:bg-gray-700 dark:border-gray-300 dark:text-gray-300
|
@apply text-gray-600 bg-white border-gray-400 dark:bg-gray-700 dark:border-gray-300 dark:text-gray-300
|
||||||
}
|
}
|
||||||
|
@ -68,11 +72,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-btn-primary {
|
.clr-btn-primary {
|
||||||
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-400
|
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-600
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-btn-default {
|
.clr-btn-default {
|
||||||
@apply text-gray-500 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 bg-gray-100 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400
|
@apply text-gray-600 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 bg-[#f0f2f7] hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transparent button */
|
/* Transparent button */
|
||||||
|
|
|
@ -10,12 +10,12 @@ import SubmitButton from '../components/Common/SubmitButton';
|
||||||
import TextArea from '../components/Common/TextArea';
|
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 { useLibrary } from '../context/LibraryContext';
|
||||||
import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
|
import { IRSFormCreateData } from '../utils/models';
|
||||||
|
|
||||||
function CreateRSFormPage() {
|
function CreateRSFormPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { createSchema, error, setError, loading } = useNewRSForm()
|
const { createSchema, error, setError, processing } = useLibrary();
|
||||||
|
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
|
@ -34,15 +34,10 @@ function CreateRSFormPage() {
|
||||||
setFile(undefined);
|
setFile(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSuccess(newSchema: IRSFormMeta) {
|
|
||||||
toast.success('Схема успешно создана');
|
|
||||||
navigate(`/rsforms/${newSchema.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (loading) {
|
if (processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: IRSFormCreateData = {
|
const data: IRSFormCreateData = {
|
||||||
|
@ -53,7 +48,10 @@ function CreateRSFormPage() {
|
||||||
file: file,
|
file: file,
|
||||||
fileName: file?.name
|
fileName: file?.name
|
||||||
};
|
};
|
||||||
createSchema(data, onSuccess);
|
createSchema(data, (newSchema) => {
|
||||||
|
toast.success('Схема успешно создана');
|
||||||
|
navigate(`/rsforms/${newSchema.id}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -92,7 +90,7 @@ function CreateRSFormPage() {
|
||||||
<div className='flex items-center justify-center py-2 mt-4'>
|
<div className='flex items-center justify-center py-2 mt-4'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Создать схему'
|
text='Создать схему'
|
||||||
loading={loading}
|
loading={processing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{ error && <BackendError error={error} />}
|
{ error && <BackendError error={error} />}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useLayoutEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
@ -5,11 +6,14 @@ import { useAuth } from '../context/AuthContext';
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
if (!user) {
|
|
||||||
navigate('/library?filter=common');
|
useLayoutEffect(() => {
|
||||||
} else if(!user.is_staff) {
|
if (!user) {
|
||||||
navigate('/library?filter=personal');
|
navigate('/library?filter=common');
|
||||||
}
|
} else if(!user.is_staff) {
|
||||||
|
navigate('/library?filter=personal');
|
||||||
|
}
|
||||||
|
}, [navigate, user])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center justify-center w-full py-2'>
|
<div className='flex flex-col items-center justify-center w-full py-2'>
|
||||||
|
|
|
@ -70,7 +70,7 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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 TextURL from '../components/Common/TextURL';
|
||||||
import InfoMessage from '../components/InfoMessage';
|
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { IUserLoginData } from '../utils/models';
|
import { IUserLoginData } from '../utils/models';
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ function LoginPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-2'> { user
|
<div className='w-full py-2'> { user
|
||||||
? <InfoMessage message={`Вы вошли в систему как ${user.username}`} />
|
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
||||||
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
|
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
|
||||||
<TextInput id='username'
|
<TextInput id='username'
|
||||||
label='Имя пользователя'
|
label='Имя пользователя'
|
||||||
|
|
|
@ -16,10 +16,6 @@ interface DlgShowASTProps {
|
||||||
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(
|
const nodes: GraphNode[] = useMemo(
|
||||||
() => syntaxTree.map(node => {
|
() => syntaxTree.map(node => {
|
||||||
return {
|
return {
|
||||||
|
@ -46,14 +42,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
hideWindow={hideWindow}
|
hideWindow={hideWindow}
|
||||||
onSubmit={handleSubmit}
|
readonly
|
||||||
submitText='Закрыть'
|
|
||||||
canSubmit={true}
|
|
||||||
>
|
>
|
||||||
<div className='flex flex-col items-start gap-2'>
|
<div className='flex flex-col items-start gap-2'>
|
||||||
<div className='w-full text-lg text-center'>{expression}</div>
|
<div className='w-full text-lg text-center'>{expression}</div>
|
||||||
<div className='flex-wrap w-full h-full overflow-auto'>
|
<div className='flex-wrap w-full h-full overflow-auto'>
|
||||||
<div className='relative w-[1040px] h-[600px] 2xl:w-[1680px] 2xl:h-[600px]'>
|
<div className='relative w-[1040px] h-[600px] 2xl:w-[1680px] 2xl:h-[600px] max-h-full max-w-full'>
|
||||||
<GraphCanvas
|
<GraphCanvas
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||||
|
import Divider from '../../components/Common/Divider';
|
||||||
import MiniButton from '../../components/Common/MiniButton';
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
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, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
||||||
import { getCstTypeLabel } from '../../utils/staticUI';
|
import { getCstTypeLabel, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import EditorRSExpression from './EditorRSExpression';
|
||||||
import ViewSideConstituents from './elements/ViewSideConstituents';
|
import ViewSideConstituents from './elements/ViewSideConstituents';
|
||||||
|
|
||||||
|
@ -62,9 +64,11 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
||||||
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 ? getCstTypificationLabel(activeCst) : 'N/A');
|
||||||
|
} else if (schema && schema?.items.length > 0) {
|
||||||
|
onOpenEdit(schema.items[0].id);
|
||||||
}
|
}
|
||||||
}, [activeCst]);
|
}, [activeCst, onOpenEdit, schema]);
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -105,12 +109,12 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-start w-full gap-2'>
|
<div className='flex items-stretch w-full gap-2 mb-2'>
|
||||||
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min px-4 py-2 border'>
|
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min max-h-fit px-4 py-2 border'>
|
||||||
<div className='flex items-start justify-between'>
|
<div className='flex items-start justify-between'>
|
||||||
<button type='submit'
|
<button type='submit'
|
||||||
title='Сохранить изменения'
|
title='Сохранить изменения'
|
||||||
className='px-1 py-1 font-bold rounded whitespace-nowrap disabled:cursor-not-allowed clr-btn-primary'
|
className='px-1 py-1 font-bold border rounded whitespace-nowrap disabled:cursor-not-allowed clr-btn-primary'
|
||||||
disabled={!isModified || !isEnabled}
|
disabled={!isModified || !isEnabled}
|
||||||
>
|
>
|
||||||
<SaveIcon size={5} />
|
<SaveIcon size={5} />
|
||||||
|
@ -150,6 +154,36 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />}
|
icon={<DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />}
|
||||||
/>
|
/>
|
||||||
|
<div id='cst-help' className='flex items-center ml-[0.25rem]'>
|
||||||
|
<HelpIcon color='text-primary' size={5} />
|
||||||
|
</div>
|
||||||
|
<ConceptTooltip anchorSelect='#cst-help'>
|
||||||
|
<div className='max-w-[35rem]'>
|
||||||
|
<h1>Подсказки</h1>
|
||||||
|
<p><b className='text-red'>Изменения сохраняются ПОСЛЕ нажатия на кнопку снизу или слева вверху</b></p>
|
||||||
|
<p><b>Клик на формальное выражение</b> - обратите внимание на кнопки снизу.<br/>Для каждой есть горячая клавиша в подсказке</p>
|
||||||
|
<p><b>Список конституент справа</b> - обратите внимание на настройки фильтрации</p>
|
||||||
|
<p>- слева от ввода текста настраивается набор атрибутов конституенты</p>
|
||||||
|
<p>- справа от ввода текста настраивается список конституент, которые фильтруются</p>
|
||||||
|
<p>- текущая конституента выделена цветом строки</p>
|
||||||
|
<p>- двойной клик / Alt + клик - выбор редактируемой конституенты</p>
|
||||||
|
<p>- при наведении на ID конституенты отображаются ее атрибуты</p>
|
||||||
|
<p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p>
|
||||||
|
<Divider margins='mt-2' />
|
||||||
|
<h1>Статусы</h1>
|
||||||
|
{ [... mapStatusInfo.values()].map(info => {
|
||||||
|
return (<p className='py-1'>
|
||||||
|
<span className={`inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}>
|
||||||
|
{info.text}
|
||||||
|
</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span>
|
||||||
|
{info.tooltip}
|
||||||
|
</span>
|
||||||
|
</p>);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ConceptTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TextArea id='term' label='Термин'
|
<TextArea id='term' label='Термин'
|
||||||
|
@ -167,6 +201,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<EditorRSExpression id='expression' label='Формальное выражение'
|
<EditorRSExpression id='expression' label='Формальное выражение'
|
||||||
|
activeCst={activeCst}
|
||||||
placeholder='Родоструктурное выражение, задающее формальное определение'
|
placeholder='Родоструктурное выражение, задающее формальное определение'
|
||||||
value={expression}
|
value={expression}
|
||||||
disabled={!isEnabled}
|
disabled={!isEnabled}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import { prefixes } from '../../utils/constants';
|
import { prefixes } from '../../utils/constants';
|
||||||
import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models'
|
import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models'
|
||||||
import { getCstTypePrefix, getCstTypeShortcut, getTypeLabel, mapStatusInfo } from '../../utils/staticUI';
|
import { getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
|
||||||
|
|
||||||
interface EditorItemsProps {
|
interface EditorItemsProps {
|
||||||
onOpenEdit: (cstID: number) => void
|
onOpenEdit: (cstID: number) => void
|
||||||
|
@ -192,10 +192,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
{
|
{
|
||||||
name: 'Тип',
|
name: 'Тип',
|
||||||
id: 'type',
|
id: 'type',
|
||||||
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getTypeLabel(cst)}</div>,
|
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getCstTypificationLabel(cst)}</div>,
|
||||||
width: '140px',
|
width: '175px',
|
||||||
minWidth: '100px',
|
maxWidth: '175px',
|
||||||
maxWidth: '140px',
|
|
||||||
wrap: true,
|
wrap: true,
|
||||||
reorder: true,
|
reorder: true,
|
||||||
hide: 1600
|
hide: 1600
|
||||||
|
@ -249,7 +248,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div
|
<div
|
||||||
className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app' +
|
className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app' +
|
||||||
(!noNavigation ? ' sticky z-10 top-[4rem]' : ' sticky z-10 top-[0rem]')}
|
(!noNavigation ? ' sticky z-10 top-[0rem]' : ' sticky z-10 top-[0rem]')}
|
||||||
>
|
>
|
||||||
<div className='mr-3 whitespace-nowrap'>
|
<div className='mr-3 whitespace-nowrap'>
|
||||||
Выбраны
|
Выбраны
|
||||||
|
@ -257,25 +256,25 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0}
|
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isEditable && <div className='flex items-center justify-start w-full gap-1'>
|
<div className='flex items-center justify-start w-full gap-1'>
|
||||||
<Button
|
<Button
|
||||||
tooltip='Переместить вверх'
|
tooltip='Переместить вверх'
|
||||||
icon={<ArrowUpIcon size={6}/>}
|
icon={<ArrowUpIcon size={6}/>}
|
||||||
disabled={nothingSelected}
|
disabled={!isEditable || nothingSelected}
|
||||||
dense
|
dense
|
||||||
onClick={handleMoveUp}
|
onClick={handleMoveUp}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
tooltip='Переместить вниз'
|
tooltip='Переместить вниз'
|
||||||
icon={<ArrowDownIcon size={6}/>}
|
icon={<ArrowDownIcon size={6}/>}
|
||||||
disabled={nothingSelected}
|
disabled={!isEditable || nothingSelected}
|
||||||
dense
|
dense
|
||||||
onClick={handleMoveDown}
|
onClick={handleMoveDown}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
tooltip='Удалить выбранные'
|
tooltip='Удалить выбранные'
|
||||||
icon={<DumpBinIcon color={!nothingSelected ? 'text-red' : ''} size={6}/>}
|
icon={<DumpBinIcon color={!nothingSelected ? 'text-red' : ''} size={6}/>}
|
||||||
disabled={nothingSelected}
|
disabled={!isEditable || nothingSelected}
|
||||||
dense
|
dense
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
|
@ -284,12 +283,14 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
tooltip='Переиндексировать имена'
|
tooltip='Переиндексировать имена'
|
||||||
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
|
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
|
||||||
dense
|
dense
|
||||||
|
disabled={!isEditable}
|
||||||
onClick={handleReindex}
|
onClick={handleReindex}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
tooltip='Новая конституента'
|
tooltip='Новая конституента'
|
||||||
icon={<SmallPlusIcon color='text-green' size={6}/>}
|
icon={<SmallPlusIcon color='text-green' size={6}/>}
|
||||||
dense
|
dense
|
||||||
|
disabled={!isEditable}
|
||||||
onClick={() => handleCreateCst()}
|
onClick={() => handleCreateCst()}
|
||||||
/>
|
/>
|
||||||
{(Object.values(CstType)).map(
|
{(Object.values(CstType)).map(
|
||||||
|
@ -299,6 +300,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
text={`${getCstTypePrefix(type)}`}
|
text={`${getCstTypePrefix(type)}`}
|
||||||
tooltip={getCstTypeShortcut(type)}
|
tooltip={getCstTypeShortcut(type)}
|
||||||
dense
|
dense
|
||||||
|
disabled={!isEditable}
|
||||||
onClick={() => handleCreateCst(type)}
|
onClick={() => handleCreateCst(type)}
|
||||||
/>;
|
/>;
|
||||||
})}
|
})}
|
||||||
|
@ -328,7 +330,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ConceptTooltip>
|
</ConceptTooltip>
|
||||||
</div>}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-full' onKeyDown={handleTableKey}>
|
<div className='w-full h-full' onKeyDown={handleTableKey}>
|
||||||
<ConceptDataTable
|
<ConceptDataTable
|
||||||
|
|
|
@ -7,8 +7,8 @@ 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 { TokenID } from '../../utils/enums';
|
import { TokenID } from '../../utils/enums';
|
||||||
import { IRSErrorDescription, SyntaxTree } from '../../utils/models';
|
import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models';
|
||||||
import { getCstExpressionPrefix } from '../../utils/staticUI';
|
import { getCstExpressionPrefix, getTypificationLabel } from '../../utils/staticUI';
|
||||||
import ParsingResult from './elements/ParsingResult';
|
import ParsingResult from './elements/ParsingResult';
|
||||||
import RSLocalButton from './elements/RSLocalButton';
|
import RSLocalButton from './elements/RSLocalButton';
|
||||||
import RSTokenButton from './elements/RSTokenButton';
|
import RSTokenButton from './elements/RSTokenButton';
|
||||||
|
@ -17,6 +17,7 @@ import { getSymbolSubstitute, TextWrapper } from './elements/textEditing';
|
||||||
|
|
||||||
interface EditorRSExpressionProps {
|
interface EditorRSExpressionProps {
|
||||||
id: string
|
id: string
|
||||||
|
activeCst?: IConstituenta
|
||||||
label: string
|
label: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
@ -30,10 +31,10 @@ interface EditorRSExpressionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSExpression({
|
function EditorRSExpression({
|
||||||
id, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
||||||
toggleEditMode, setTypification, onChange
|
toggleEditMode, setTypification, onChange
|
||||||
}: EditorRSExpressionProps) {
|
}: EditorRSExpressionProps) {
|
||||||
const { schema, activeCst } = useRSForm();
|
const { schema } = useRSForm();
|
||||||
const [isModified, setIsModified] = useState(false);
|
const [isModified, setIsModified] = useState(false);
|
||||||
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
|
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
|
||||||
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
|
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
|
||||||
|
@ -66,7 +67,11 @@ function EditorRSExpression({
|
||||||
}
|
}
|
||||||
expressionCtrl.current!.focus();
|
expressionCtrl.current!.focus();
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
setTypification(parse.typification);
|
setTypification(getTypificationLabel({
|
||||||
|
isValid: parse.parseResult,
|
||||||
|
resultType: parse.typification,
|
||||||
|
args: parse.args
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,13 +210,22 @@ function EditorRSExpression({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3 w-full'>
|
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3 w-full'>
|
||||||
|
<div className='relative w-full'>
|
||||||
|
<div className='absolute top-[-0.3rem] right-0'>
|
||||||
|
<StatusBar
|
||||||
|
isModified={isModified}
|
||||||
|
constituenta={activeCst}
|
||||||
|
parseData={parseData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Label
|
<Label
|
||||||
text={label}
|
text={label}
|
||||||
required={false}
|
required={false}
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
/>
|
/>
|
||||||
<textarea id={id} ref={expressionCtrl}
|
<textarea id={id} ref={expressionCtrl}
|
||||||
className='w-full px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800'
|
className='w-full px-3 py-2 mt-2 leading-tight border shadow clr-input'
|
||||||
rows={6}
|
rows={6}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -223,23 +237,15 @@ function EditorRSExpression({
|
||||||
/>
|
/>
|
||||||
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
|
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
{isActive && <StatusBar
|
|
||||||
isModified={isModified}
|
|
||||||
constituenta={activeCst}
|
|
||||||
parseData={parseData}
|
|
||||||
/>}
|
|
||||||
<Button
|
<Button
|
||||||
tooltip='Проверить формальное выражение'
|
tooltip='Проверить формальное выражение'
|
||||||
text='Проверить'
|
text='Проверить'
|
||||||
|
widthClass='h-full w-fit'
|
||||||
|
colorClass='clr-btn-default'
|
||||||
onClick={handleCheckExpression}
|
onClick={handleCheckExpression}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isActive && EditButtons}
|
{isActive && EditButtons}
|
||||||
{!isActive && <StatusBar
|
|
||||||
isModified={isModified}
|
|
||||||
constituenta={activeCst}
|
|
||||||
parseData={parseData}
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
{ (loading || parseData) &&
|
{ (loading || parseData) &&
|
||||||
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'>
|
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'>
|
||||||
|
|
|
@ -1,35 +1,64 @@
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, useSelection } from 'reagraph';
|
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph';
|
||||||
|
|
||||||
import Button from '../../components/Common/Button';
|
import Button from '../../components/Common/Button';
|
||||||
import Checkbox from '../../components/Common/Checkbox';
|
import Checkbox from '../../components/Common/Checkbox';
|
||||||
import ConceptSelect from '../../components/Common/ConceptSelect';
|
import ConceptSelect from '../../components/Common/ConceptSelect';
|
||||||
|
import { ArrowsRotateIcon } from '../../components/Icons';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||||
import { resources } from '../../utils/constants';
|
import { resources } from '../../utils/constants';
|
||||||
|
import { Graph } from '../../utils/Graph';
|
||||||
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
|
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
|
||||||
|
|
||||||
function EditorTermGraph() {
|
function EditorTermGraph() {
|
||||||
const { schema } = useRSForm();
|
const { schema } = useRSForm();
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode, noNavigation } = useConceptTheme();
|
||||||
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'forceatlas2');
|
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
|
||||||
|
|
||||||
|
const [ filtered, setFiltered ] = useState<Graph>(new Graph());
|
||||||
const [ orbit, setOrbit ] = useState(false);
|
const [ orbit, setOrbit ] = useState(false);
|
||||||
|
const [ noHermits, setNoHermits ] = useLocalStorage('graph_no_hermits', true);
|
||||||
|
const [ noTransitive, setNoTransitive ] = useLocalStorage('graph_no_transitive', false);
|
||||||
const graphRef = useRef<GraphCanvasRef | null>(null);
|
const graphRef = useRef<GraphCanvasRef | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!schema) {
|
||||||
|
setFiltered(new Graph());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const graph = schema.graph.clone();
|
||||||
|
if (noHermits) {
|
||||||
|
graph.removeIsolated();
|
||||||
|
}
|
||||||
|
if (noTransitive) {
|
||||||
|
graph.transitiveReduction();
|
||||||
|
}
|
||||||
|
setFiltered(graph);
|
||||||
|
}, [schema, noHermits, noTransitive]);
|
||||||
|
|
||||||
const nodes: GraphNode[] = useMemo(() => {
|
const nodes: GraphNode[] = useMemo(() => {
|
||||||
return schema?.items.map(cst => {
|
const result: GraphNode[] = [];
|
||||||
return {
|
if (!schema) {
|
||||||
id: String(cst.id),
|
return result;
|
||||||
label: (cst.term.resolved || cst.term.raw) ? `${cst.alias}: ${cst.term.resolved || cst.term.raw}` : cst.alias
|
}
|
||||||
}}
|
filtered.nodes.forEach(node => {
|
||||||
) ?? [];
|
const cst = schema.items.find(cst => cst.id === node.id);
|
||||||
}, [schema?.items]);
|
if (cst) {
|
||||||
|
result.push({
|
||||||
|
id: String(node.id),
|
||||||
|
label: cst.term.resolved ? `${cst.alias}: ${cst.term.resolved}` : cst.alias
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [schema, filtered.nodes]);
|
||||||
|
|
||||||
const edges: GraphEdge[] = useMemo(() => {
|
const edges: GraphEdge[] = useMemo(() => {
|
||||||
const result: GraphEdge[] = [];
|
const result: GraphEdge[] = [];
|
||||||
let edgeID = 1;
|
let edgeID = 1;
|
||||||
schema?.graph.nodes.forEach(source => {
|
filtered.nodes.forEach(source => {
|
||||||
source.outputs.forEach(target => {
|
source.outputs.forEach(target => {
|
||||||
result.push({
|
result.push({
|
||||||
id: String(edgeID),
|
id: String(edgeID),
|
||||||
|
@ -40,7 +69,7 @@ function EditorTermGraph() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}, [schema?.graph]);
|
}, [filtered.nodes]);
|
||||||
|
|
||||||
const handleCenter = useCallback(() => {
|
const handleCenter = useCallback(() => {
|
||||||
graphRef.current?.resetControls();
|
graphRef.current?.resetControls();
|
||||||
|
@ -62,29 +91,47 @@ function EditorTermGraph() {
|
||||||
focusOnSelect: false
|
focusOnSelect: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canvasSize = !noNavigation ?
|
||||||
|
'w-[1240px] h-[736px] 2xl:w-[1880px] 2xl:h-[746px]'
|
||||||
|
: 'w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[810px]';
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div className='relative w-full'>
|
<div className='relative w-full'>
|
||||||
<div className='absolute top-0 left-0 z-20 px-3 py-2 w-[12rem] flex flex-col gap-2'>
|
<div className='absolute top-0 left-0 z-20 py-2 w-[12rem] flex flex-col'>
|
||||||
<ConceptSelect
|
<div className='flex items-center gap-1 w-[15rem]'>
|
||||||
options={GraphLayoutSelector}
|
<Button
|
||||||
placeholder='Выберите тип'
|
icon={<ArrowsRotateIcon size={8} />}
|
||||||
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
|
dense
|
||||||
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
|
tooltip='Центрировать изображение'
|
||||||
/>
|
widthClass='h-full'
|
||||||
|
onClick={handleCenter}
|
||||||
|
/>
|
||||||
|
<ConceptSelect
|
||||||
|
options={GraphLayoutSelector}
|
||||||
|
placeholder='Выберите тип'
|
||||||
|
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []}
|
||||||
|
onChange={data => { setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value); }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label='Анимация вращения'
|
label='Анимация вращения'
|
||||||
widthClass='w-full'
|
|
||||||
value={orbit}
|
value={orbit}
|
||||||
onChange={ event => setOrbit(event.target.checked) }/>
|
onChange={ event => setOrbit(event.target.checked) }
|
||||||
<Button
|
/>
|
||||||
text='Центрировать'
|
<Checkbox
|
||||||
dense
|
label='Удалить несвязанные'
|
||||||
onClick={handleCenter}
|
value={noHermits}
|
||||||
|
onChange={ event => setNoHermits(event.target.checked) }
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label='Транзитивная редукция'
|
||||||
|
value={noTransitive}
|
||||||
|
onChange={ event => setNoTransitive(event.target.checked) }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-wrap w-full h-full overflow-auto'>
|
<div className='flex-wrap w-full h-full overflow-auto'>
|
||||||
<div className='relative w-[1240px] h-[800px] 2xl:w-[1880px] 2xl:h-[800px]'>
|
<div className={`relative border-t border-r ${canvasSize}`}>
|
||||||
<GraphCanvas
|
<GraphCanvas
|
||||||
draggable
|
draggable
|
||||||
ref={graphRef}
|
ref={graphRef}
|
||||||
|
@ -99,9 +146,12 @@ function EditorTermGraph() {
|
||||||
onNodePointerOver={onNodePointerOver}
|
onNodePointerOver={onNodePointerOver}
|
||||||
onNodePointerOut={onNodePointerOut}
|
onNodePointerOut={onNodePointerOut}
|
||||||
cameraMode={ orbit ? 'orbit' : layout.includes('3d') ? 'rotate' : 'pan'}
|
cameraMode={ orbit ? 'orbit' : layout.includes('3d') ? 'rotate' : 'pan'}
|
||||||
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: 1 } : undefined }
|
layoutOverrides={ layout.includes('tree') ? { nodeLevelRatio: schema && schema?.items.length < 50 ? 3 : 1 } : undefined }
|
||||||
labelFontUrl={resources.graph_font}
|
labelFontUrl={resources.graph_font}
|
||||||
theme={darkMode ? darkTheme : lightTheme}
|
theme={darkMode ? darkTheme : lightTheme}
|
||||||
|
renderNode={({ node, ...rest }) => (
|
||||||
|
<Sphere {...rest} node={node} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,7 +27,6 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
||||||
} = useRSForm();
|
} = useRSForm();
|
||||||
const schemaMenu = useDropdown();
|
const schemaMenu = useDropdown();
|
||||||
const editMenu = useDropdown();
|
const editMenu = useDropdown();
|
||||||
|
|
||||||
|
|
||||||
const handleClaimOwner = useCallback(() => {
|
const handleClaimOwner = useCallback(() => {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
|
@ -61,12 +60,13 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
||||||
}, [schemaMenu]);
|
}, [schemaMenu]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center w-fit'>
|
<div className='flex items-stretch w-fit'>
|
||||||
<div ref={schemaMenu.ref}>
|
<div ref={schemaMenu.ref}>
|
||||||
<Button
|
<Button
|
||||||
tooltip='Действия'
|
tooltip='Действия'
|
||||||
icon={<MenuIcon size={5}/>}
|
icon={<MenuIcon size={5}/>}
|
||||||
borderClass=''
|
borderClass=''
|
||||||
|
widthClass='h-full w-fit'
|
||||||
dense
|
dense
|
||||||
onClick={schemaMenu.toggle}
|
onClick={schemaMenu.toggle}
|
||||||
/>
|
/>
|
||||||
|
@ -108,6 +108,7 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
||||||
<Button
|
<Button
|
||||||
tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')}
|
tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')}
|
||||||
borderClass=''
|
borderClass=''
|
||||||
|
widthClass='h-full w-fit'
|
||||||
icon={<PenIcon size={5} color={isEditable ? 'text-green' : 'text-red'}/>}
|
icon={<PenIcon size={5} color={isEditable ? 'text-green' : 'text-red'}/>}
|
||||||
dense
|
dense
|
||||||
onClick={editMenu.toggle}
|
onClick={editMenu.toggle}
|
||||||
|
@ -144,6 +145,7 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
||||||
? <EyeIcon color='text-primary' size={5}/>
|
? <EyeIcon color='text-primary' size={5}/>
|
||||||
: <EyeOffIcon size={5}/>
|
: <EyeOffIcon size={5}/>
|
||||||
}
|
}
|
||||||
|
widthClass='h-full w-fit'
|
||||||
borderClass=''
|
borderClass=''
|
||||||
dense
|
dense
|
||||||
onClick={toggleTracking}
|
onClick={toggleTracking}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
|
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
|
||||||
import { IConstituenta } from '../../../utils/models';
|
import { IConstituenta } from '../../../utils/models';
|
||||||
import { getTypeLabel } from '../../../utils/staticUI';
|
import { getCstTypificationLabel } from '../../../utils/staticUI';
|
||||||
|
|
||||||
interface ConstituentaTooltipProps {
|
interface ConstituentaTooltipProps {
|
||||||
data: IConstituenta
|
data: IConstituenta
|
||||||
|
@ -14,7 +14,7 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
||||||
className='max-w-[25rem] min-w-[25rem]'
|
className='max-w-[25rem] min-w-[25rem]'
|
||||||
>
|
>
|
||||||
<h1>Конституента {data.alias}</h1>
|
<h1>Конституента {data.alias}</h1>
|
||||||
<p><b>Типизация: </b>{getTypeLabel(data)}</p>
|
<p><b>Типизация: </b>{getCstTypificationLabel(data)}</p>
|
||||||
<p><b>Термин: </b>{data.term.resolved || data.term.raw}</p>
|
<p><b>Термин: </b>{data.term.resolved || data.term.raw}</p>
|
||||||
{data.definition.formal && <p><b>Выражение: </b>{data.definition.formal}</p>}
|
{data.definition.formal && <p><b>Выражение: </b>{data.definition.formal}</p>}
|
||||||
{data.definition.text.resolved && <p><b>Определение: </b>{data.definition.text.resolved}</p>}
|
{data.definition.text.resolved && <p><b>Определение: </b>{data.definition.text.resolved}</p>}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Dropdown from '../../../components/Common/Dropdown';
|
||||||
|
import DropdownButton from '../../../components/Common/DropdownButton';
|
||||||
|
import useDropdown from '../../../hooks/useDropdown';
|
||||||
|
import { DependencyMode } from '../../../utils/models';
|
||||||
|
import { getDependencyLabel } from '../../../utils/staticUI';
|
||||||
|
|
||||||
|
interface DependencyModePickerProps {
|
||||||
|
value: DependencyMode
|
||||||
|
onChange: (value: DependencyMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function DependencyModePicker({ value, onChange }: DependencyModePickerProps) {
|
||||||
|
const pickerMenu = useDropdown();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newValue: DependencyMode) => {
|
||||||
|
pickerMenu.hide();
|
||||||
|
onChange(newValue);
|
||||||
|
}, [pickerMenu, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={pickerMenu.ref}>
|
||||||
|
<span
|
||||||
|
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={pickerMenu.toggle}
|
||||||
|
>
|
||||||
|
{getDependencyLabel(value)}
|
||||||
|
</span>
|
||||||
|
{ pickerMenu.isActive &&
|
||||||
|
<Dropdown stretchLeft >
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.ALL)}>
|
||||||
|
<p><b>вся схема:</b> список всех конституент схемы</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.EXPRESSION)}>
|
||||||
|
<p><b>выражение:</b> список идентификаторов из выражения</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.OUTPUTS)}>
|
||||||
|
<p><b>потребители:</b> конституенты, ссылающиеся на данную</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.INPUTS)}>
|
||||||
|
<p><b>поставщики:</b> конституенты, на которые ссылается данная</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_OUTPUTS)}>
|
||||||
|
<p><b>зависимые:</b> конституенты, зависящие по цепочке</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(DependencyMode.EXPAND_INPUTS)}>
|
||||||
|
<p><b>влияющие:</b> конституенты, влияющие на данную (цепочка)</p>
|
||||||
|
</DropdownButton>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// case DependencyMode.OUTPUTS: return 'потребители';
|
||||||
|
// case DependencyMode.INPUTS: return 'поставщики';
|
||||||
|
// case DependencyMode.EXPAND_INPUTS: return 'влияющие';
|
||||||
|
// case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DependencyModePicker;
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Dropdown from '../../../components/Common/Dropdown';
|
||||||
|
import DropdownButton from '../../../components/Common/DropdownButton';
|
||||||
|
import useDropdown from '../../../hooks/useDropdown';
|
||||||
|
import { CstMatchMode } from '../../../utils/models';
|
||||||
|
import { getCstCompareLabel } from '../../../utils/staticUI';
|
||||||
|
|
||||||
|
interface MatchModePickerProps {
|
||||||
|
value: CstMatchMode
|
||||||
|
onChange: (value: CstMatchMode) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchModePicker({ value, onChange }: MatchModePickerProps) {
|
||||||
|
const pickerMenu = useDropdown();
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newValue: CstMatchMode) => {
|
||||||
|
pickerMenu.hide();
|
||||||
|
onChange(newValue);
|
||||||
|
}, [pickerMenu, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={pickerMenu.ref}>
|
||||||
|
<span
|
||||||
|
className='text-sm font-semibold underline cursor-pointer select-none whitespace-nowrap'
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={pickerMenu.toggle}
|
||||||
|
>
|
||||||
|
{getCstCompareLabel(value)}
|
||||||
|
</span>
|
||||||
|
{ pickerMenu.isActive &&
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownButton onClick={() => handleChange(CstMatchMode.ALL)}>
|
||||||
|
<p><b>везде:</b> искать во всех атрибутах</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(CstMatchMode.EXPR)}>
|
||||||
|
<p><b>выраж:</b> искать в формальных выражениях</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(CstMatchMode.TERM)}>
|
||||||
|
<p><b>термин:</b> искать в терминах</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(CstMatchMode.TEXT)}>
|
||||||
|
<p><b>текст:</b> искать в определениях и конвенциях</p>
|
||||||
|
</DropdownButton>
|
||||||
|
<DropdownButton onClick={() => handleChange(CstMatchMode.NAME)}>
|
||||||
|
<p><b>ID:</b> искать в идентификаторах конституент</p>
|
||||||
|
</DropdownButton>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MatchModePicker;
|
|
@ -1,4 +1,3 @@
|
||||||
import Card from '../../../components/Common/Card';
|
|
||||||
import Divider from '../../../components/Common/Divider';
|
import Divider from '../../../components/Common/Divider';
|
||||||
import LabeledText from '../../../components/Common/LabeledText';
|
import LabeledText from '../../../components/Common/LabeledText';
|
||||||
import { type IRSFormStats } from '../../../utils/models';
|
import { type IRSFormStats } from '../../../utils/models';
|
||||||
|
@ -9,7 +8,7 @@ interface RSFormStatsProps {
|
||||||
|
|
||||||
function RSFormStats({ stats }: RSFormStatsProps) {
|
function RSFormStats({ stats }: RSFormStatsProps) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className='px-4 py-2 border'>
|
||||||
<LabeledText id='count_all'
|
<LabeledText id='count_all'
|
||||||
label='Всего конституент '
|
label='Всего конституент '
|
||||||
text={stats.count_all}
|
text={stats.count_all}
|
||||||
|
@ -28,12 +27,12 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
||||||
label='Невычислимы '
|
label='Невычислимы '
|
||||||
text={stats.count_incalc}
|
text={stats.count_incalc}
|
||||||
/>}
|
/>}
|
||||||
<Divider />
|
<Divider margins='my-1' />
|
||||||
<LabeledText id='count_termin'
|
<LabeledText id='count_termin'
|
||||||
label='Термины '
|
label='Термины '
|
||||||
text={stats.count_termin}
|
text={stats.count_termin}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider margins='my-1' />
|
||||||
{ stats.count_base > 0 &&
|
{ stats.count_base > 0 &&
|
||||||
<LabeledText id='count_base'
|
<LabeledText id='count_base'
|
||||||
label='Базисные множества '
|
label='Базисные множества '
|
||||||
|
@ -74,7 +73,7 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
||||||
label='Теормы '
|
label='Теормы '
|
||||||
text={stats.count_theorem}
|
text={stats.count_theorem}
|
||||||
/>}
|
/>}
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
|
||||||
const data = mapStatusInfo.get(status)!;
|
const data = mapStatusInfo.get(status)!;
|
||||||
return (
|
return (
|
||||||
<div title={data.tooltip}
|
<div title={data.tooltip}
|
||||||
className={'min-h-[2rem] min-w-[6rem] font-semibold inline-flex border rounded-lg items-center justify-center align-middle ' + data.color}>
|
className={`text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center justify-center align-middle ${data.color}`}>
|
||||||
{data.text}
|
Статус: [ {data.text} ]
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Checkbox from '../../../components/Common/Checkbox';
|
|
||||||
import ConceptDataTable from '../../../components/Common/ConceptDataTable';
|
import ConceptDataTable from '../../../components/Common/ConceptDataTable';
|
||||||
import { useRSForm } from '../../../context/RSFormContext';
|
import { useRSForm } from '../../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||||
import useLocalStorage from '../../../hooks/useLocalStorage';
|
import useLocalStorage from '../../../hooks/useLocalStorage';
|
||||||
import { prefixes } from '../../../utils/constants';
|
import { prefixes } from '../../../utils/constants';
|
||||||
import { CstType, extractGlobals,type IConstituenta, matchConstituenta } from '../../../utils/models';
|
import { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models';
|
||||||
import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
|
import { getCstDescription, getMockConstituenta, mapStatusInfo } from '../../../utils/staticUI';
|
||||||
import ConstituentaTooltip from './ConstituentaTooltip';
|
import ConstituentaTooltip from './ConstituentaTooltip';
|
||||||
|
import DependencyModePicker from './DependencyModePicker';
|
||||||
|
import MatchModePicker from './MatchModePicker';
|
||||||
|
|
||||||
interface ViewSideConstituentsProps {
|
interface ViewSideConstituentsProps {
|
||||||
expression: string
|
expression: string
|
||||||
|
@ -19,31 +20,38 @@ interface ViewSideConstituentsProps {
|
||||||
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
||||||
const { darkMode } = useConceptTheme();
|
const { darkMode } = useConceptTheme();
|
||||||
const { schema } = useRSForm();
|
const { schema } = useRSForm();
|
||||||
|
|
||||||
|
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
|
||||||
|
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
|
||||||
|
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', DependencyMode.ALL);
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||||
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
|
|
||||||
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!schema?.items) {
|
if (!schema?.items) {
|
||||||
setFilteredData([]);
|
setFilteredData([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onlyExpression) {
|
let filtered: IConstituenta[] = [];
|
||||||
|
if (filterSource === DependencyMode.EXPRESSION) {
|
||||||
const aliases = extractGlobals(expression);
|
const aliases = extractGlobals(expression);
|
||||||
const filtered = schema?.items.filter((cst) => aliases.has(cst.alias));
|
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, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
|
(alias, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
|
||||||
}
|
}
|
||||||
setFilteredData(filtered);
|
} else if (!activeID) {
|
||||||
} else if (!filterText) {
|
filtered = schema.items
|
||||||
setFilteredData(schema?.items);
|
|
||||||
} else {
|
} else {
|
||||||
setFilteredData(schema?.items.filter((cst) => matchConstituenta(filterText, cst)));
|
filtered = applyGraphFilter(schema, activeID, filterSource);
|
||||||
}
|
}
|
||||||
}, [filterText, setFilteredData, onlyExpression, expression, schema]);
|
if (filterText) {
|
||||||
|
filtered = filtered.filter((cst) => matchConstituenta(filterText, cst, filterMatch));
|
||||||
|
}
|
||||||
|
setFilteredData(filtered);
|
||||||
|
}, [filterText, setFilteredData, filterSource, expression, schema, filterMatch, activeID]);
|
||||||
|
|
||||||
const handleRowClicked = useCallback(
|
const handleRowClicked = useCallback(
|
||||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
|
@ -130,25 +138,19 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
|
||||||
}
|
}
|
||||||
], []
|
], []
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-h-[80vh] overflow-y-scroll border flex-grow w-full'>
|
<div className='max-h-[calc(100vh-10.3rem)] min-h-[40rem] overflow-y-scroll border flex-grow w-full'>
|
||||||
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b-2 border-gray-400 rounded dark:bg-gray-700 dark:border-gray-300'>
|
<div className='sticky top-0 left-0 right-0 z-10 flex items-center justify-between w-full gap-1 px-2 py-1 bg-white border-b rounded clr-bg-pop clr-border'>
|
||||||
<div className='w-full'>
|
<div className='flex items-center justify-between w-full'>
|
||||||
|
<MatchModePicker value={filterMatch} onChange={setFilterMatch}/>
|
||||||
<input type='text'
|
<input type='text'
|
||||||
className='w-full px-2 outline-none dark:bg-gray-700 hover:text-clip'
|
className='w-full px-2 bg-white outline-none hover:text-clip clr-bg-pop clr-border'
|
||||||
placeholder='текст для фильтрации списка'
|
placeholder='наберите текст фильтра'
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={event => { setFilterText(event.target.value); }}
|
onChange={event => { setFilterText(event.target.value); }}
|
||||||
disabled={onlyExpression}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='w-fit min-w-[8rem]'>
|
|
||||||
<Checkbox
|
|
||||||
label='из выражения'
|
|
||||||
value={onlyExpression}
|
|
||||||
onChange={event => { setOnlyExpression(event.target.checked); }}
|
|
||||||
/>
|
/>
|
||||||
|
<DependencyModePicker value={filterSource} onChange={setFilterSource}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConceptDataTable
|
<ConceptDataTable
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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 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';
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ function RegisterPage() {
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-2'>
|
<div className='w-full py-2'>
|
||||||
{ user &&
|
{ user &&
|
||||||
<InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> }
|
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
|
||||||
{ !user &&
|
{ !user &&
|
||||||
<Form title='Регистрация пользователя' onSubmit={handleSubmit}>
|
<Form title='Регистрация пользователя' onSubmit={handleSubmit}>
|
||||||
<TextInput id='username' label='Имя пользователя' type='text'
|
<TextInput id='username' label='Имя пользователя' type='text'
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import InfoMessage from '../components/InfoMessage';
|
|
||||||
|
|
||||||
function RestorePasswordPage() {
|
function RestorePasswordPage() {
|
||||||
return (
|
return (
|
||||||
<InfoMessage message='Функционал автоматического восстановления пароля не доступен. Обратитесь в адинистратору' />
|
<b>Функционал автоматического восстановления пароля не доступен. Обратитесь в адинистратору</b>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,51 @@ describe('Testing Graph constuction', () => {
|
||||||
expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
|
expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
|
||||||
expect([... graph.nodes.get(1)!.outputs]).toStrictEqual([2]);
|
expect([... graph.nodes.get(1)!.outputs]).toStrictEqual([2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cloning', () => {
|
||||||
|
const graph = new Graph([[1, 2], [3], [4, 1]]);
|
||||||
|
const clone = graph.clone();
|
||||||
|
expect([... graph.nodes.keys()]).toStrictEqual([... clone.nodes.keys()]);
|
||||||
|
expect([... graph.nodes.values()]).toStrictEqual([... clone.nodes.values()]);
|
||||||
|
|
||||||
|
clone.removeNode(3);
|
||||||
|
expect(clone.nodes.get(3)).toBeUndefined();
|
||||||
|
expect(graph.nodes.get(3)).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Testing Graph editing', () => {
|
||||||
|
test('removing edges should not remove nodes', () => {
|
||||||
|
const graph = new Graph([[1, 2], [3], [4, 1]]);
|
||||||
|
expect(graph.hasEdge(4, 1)).toBeTruthy();
|
||||||
|
|
||||||
|
graph.removeEdge(5, 0);
|
||||||
|
graph.removeEdge(4, 1);
|
||||||
|
|
||||||
|
expect([... graph.nodes.keys()]).toStrictEqual([1, 2, 3, 4]);
|
||||||
|
expect(graph.hasEdge(4, 1)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removing isolated nodes', () => {
|
||||||
|
const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9], [7], [8]]);
|
||||||
|
graph.removeIsolated()
|
||||||
|
expect([... graph.nodes.keys()]).toStrictEqual([9, 1, 2, 4, 3, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transitive reduction', () => {
|
||||||
|
const graph = new Graph([[1, 3], [1, 2], [2, 3]]);
|
||||||
|
graph.transitiveReduction()
|
||||||
|
expect(graph.hasEdge(1, 2)).toBeTruthy();
|
||||||
|
expect(graph.hasEdge(2, 3)).toBeTruthy();
|
||||||
|
expect(graph.hasEdge(1, 3)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Testing Graph sort', () => {
|
||||||
|
test('topological order', () => {
|
||||||
|
const graph = new Graph([[9, 1], [9, 2], [2, 1], [4, 3], [5, 9]]);
|
||||||
|
expect(graph.tolopogicalOrder()).toStrictEqual([5, 4, 3, 9, 1, 2]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Testing Graph queries', () => {
|
describe('Testing Graph queries', () => {
|
||||||
|
|
|
@ -10,6 +10,13 @@ export class GraphNode {
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): GraphNode {
|
||||||
|
const result = new GraphNode(this.id);
|
||||||
|
result.outputs = [... this.outputs];
|
||||||
|
result.inputs = [... this.inputs];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
addOutput(node: number): void {
|
addOutput(node: number): void {
|
||||||
this.outputs.push(node);
|
this.outputs.push(node);
|
||||||
}
|
}
|
||||||
|
@ -45,6 +52,12 @@ export class Graph {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): Graph {
|
||||||
|
const result = new Graph();
|
||||||
|
this.nodes.forEach(node => result.nodes.set(node.id, node.clone()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
addNode(target: number): GraphNode {
|
addNode(target: number): GraphNode {
|
||||||
let node = this.nodes.get(target);
|
let node = this.nodes.get(target);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
@ -67,6 +80,16 @@ export class Graph {
|
||||||
return nodeToRemove;
|
return nodeToRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeIsolated(): GraphNode[] {
|
||||||
|
const result: GraphNode[] = [];
|
||||||
|
this.nodes.forEach(node => {
|
||||||
|
if (node.outputs.length === 0 && node.inputs.length === 0) {
|
||||||
|
this.nodes.delete(node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
addEdge(source: number, destination: number): void {
|
addEdge(source: number, destination: number): void {
|
||||||
const sourceNode = this.addNode(source);
|
const sourceNode = this.addNode(source);
|
||||||
const destinationNode = this.addNode(destination);
|
const destinationNode = this.addNode(destination);
|
||||||
|
@ -83,6 +106,14 @@ export class Graph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasEdge(source: number, destination: number): boolean {
|
||||||
|
const sourceNode = this.nodes.get(source);
|
||||||
|
if (!sourceNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!sourceNode.outputs.find(id => id === destination);
|
||||||
|
}
|
||||||
|
|
||||||
expandOutputs(origin: number[]): number[] {
|
expandOutputs(origin: number[]): number[] {
|
||||||
const result: number[] = [];
|
const result: number[] = [];
|
||||||
const marked = new Map<number, boolean>();
|
const marked = new Map<number, boolean>();
|
||||||
|
@ -143,27 +174,63 @@ export class Graph {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitDFS(visitor: (node: GraphNode) => void) {
|
tolopogicalOrder(): number[] {
|
||||||
const visited: Map<number, boolean> = new Map();
|
const result: number[] = [];
|
||||||
|
const marked = new Map<number, boolean>();
|
||||||
this.nodes.forEach(node => {
|
this.nodes.forEach(node => {
|
||||||
if (!visited.has(node.id)) {
|
if (marked.get(node.id)) {
|
||||||
this.depthFirstSearch(node, visited, visitor);
|
return;
|
||||||
}
|
}
|
||||||
|
const toVisit: number[] = [node.id];
|
||||||
|
let index = 0;
|
||||||
|
while (toVisit.length > 0) {
|
||||||
|
const item = toVisit[index];
|
||||||
|
if (marked.get(item)) {
|
||||||
|
if (!result.find(id => id ===item)) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
toVisit.splice(index, 1);
|
||||||
|
index -= 1;
|
||||||
|
} else {
|
||||||
|
marked.set(item, true);
|
||||||
|
const itemNode = this.nodes.get(item);
|
||||||
|
if (itemNode && itemNode.outputs.length > 0) {
|
||||||
|
itemNode.outputs.forEach(child => {
|
||||||
|
if (!marked.get(child)) {
|
||||||
|
toVisit.push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (index + 1 < toVisit.length) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
marked
|
||||||
});
|
});
|
||||||
|
return result.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private depthFirstSearch(
|
transitiveReduction() {
|
||||||
node: GraphNode,
|
const order = this.tolopogicalOrder();
|
||||||
visited: Map<number, boolean>,
|
const marked = new Map<number, boolean>();
|
||||||
visitor: (node: GraphNode) => void)
|
order.forEach(nodeID => {
|
||||||
: void {
|
if (marked.get(nodeID)) {
|
||||||
visited.set(node.id, true);
|
return;
|
||||||
visitor(node);
|
}
|
||||||
node.outputs.forEach((item) => {
|
const stack: {id: number, parents: number[]}[] = [];
|
||||||
if (!visited.has(item)) {
|
stack.push({id: nodeID, parents: []});
|
||||||
const childNode = this.nodes.get(item)!;
|
while (stack.length > 0) {
|
||||||
this.depthFirstSearch(childNode, visited, visitor);
|
const item = stack.splice(0, 1)[0];
|
||||||
|
const node = this.nodes.get(item.id);
|
||||||
|
if (node) {
|
||||||
|
node.outputs.forEach(child => {
|
||||||
|
item.parents.forEach(parent => this.removeEdge(parent, child));
|
||||||
|
stack.push({id: child, parents: [item.id, ...item.parents]})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
marked.set(item.id, true)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Constants
|
// Constants
|
||||||
const prod = {
|
const prod = {
|
||||||
backend: 'http://rs.acconcept.ru:8000',
|
backend: 'https://dev.concept.ru:8000',
|
||||||
|
// backend: 'https://localhost:8000',
|
||||||
|
// backend: 'https://api.portal.concept.ru',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dev = {
|
const dev = {
|
||||||
|
@ -16,7 +18,8 @@ export const urls = {
|
||||||
exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs',
|
exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs',
|
||||||
ponomarev: 'https://inponomarev.ru/textbook',
|
ponomarev: 'https://inponomarev.ru/textbook',
|
||||||
intro_video: 'https://www.youtube.com/watch?v=0Ty9mu9sOJo',
|
intro_video: 'https://www.youtube.com/watch?v=0Ty9mu9sOJo',
|
||||||
full_course: 'https://www.youtube.com/playlist?list=PLGe_JiAwpqu1C70ruQmCm_OWTWU3KJwDo'
|
full_course: 'https://www.youtube.com/playlist?list=PLGe_JiAwpqu1C70ruQmCm_OWTWU3KJwDo',
|
||||||
|
gitrepo: 'https://github.com/IRBorisov/ConceptPortal'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resources = {
|
export const resources = {
|
||||||
|
|
|
@ -69,6 +69,11 @@ export interface ISyntaxTreeNode {
|
||||||
}
|
}
|
||||||
export type SyntaxTree = ISyntaxTreeNode[]
|
export type SyntaxTree = ISyntaxTreeNode[]
|
||||||
|
|
||||||
|
export interface IFunctionArg {
|
||||||
|
alias: string
|
||||||
|
typification: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IExpressionParse {
|
export interface IExpressionParse {
|
||||||
parseResult: boolean
|
parseResult: boolean
|
||||||
syntax: Syntax
|
syntax: Syntax
|
||||||
|
@ -77,6 +82,7 @@ export interface IExpressionParse {
|
||||||
errors: IRSErrorDescription[]
|
errors: IRSErrorDescription[]
|
||||||
astText: string
|
astText: string
|
||||||
ast: SyntaxTree
|
ast: SyntaxTree
|
||||||
|
args: IFunctionArg[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRSExpression {
|
export interface IRSExpression {
|
||||||
|
@ -118,6 +124,7 @@ export interface IConstituenta {
|
||||||
valueClass: ValueClass
|
valueClass: ValueClass
|
||||||
typification: string
|
typification: string
|
||||||
syntaxTree: string
|
syntaxTree: string
|
||||||
|
args: IFunctionArg[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +238,25 @@ export enum ExpressionStatus {
|
||||||
VERIFIED
|
VERIFIED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dependency mode for schema analysis
|
||||||
|
export enum DependencyMode {
|
||||||
|
ALL = 0,
|
||||||
|
EXPRESSION,
|
||||||
|
OUTPUTS,
|
||||||
|
INPUTS,
|
||||||
|
EXPAND_OUTPUTS,
|
||||||
|
EXPAND_INPUTS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constituent compare mode
|
||||||
|
export enum CstMatchMode {
|
||||||
|
ALL = 1,
|
||||||
|
EXPR,
|
||||||
|
TERM,
|
||||||
|
TEXT,
|
||||||
|
NAME
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Model functions =================
|
// ========== Model functions =================
|
||||||
export function inferStatus(parse?: ParsingStatus, value?: ValueClass): ExpressionStatus {
|
export function inferStatus(parse?: ParsingStatus, value?: ValueClass): ExpressionStatus {
|
||||||
if (!parse || !value) {
|
if (!parse || !value) {
|
||||||
|
@ -322,22 +348,23 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchConstituenta(query: string, target?: IConstituenta) {
|
export function matchConstituenta(query: string, target: IConstituenta, mode: CstMatchMode) {
|
||||||
if (!target) {
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.NAME) &&
|
||||||
return false;
|
target.alias.match(query)) {
|
||||||
} else if (target.alias.match(query)) {
|
|
||||||
return true;
|
return true;
|
||||||
} else if (target.term.resolved.match(query)) {
|
|
||||||
return true;
|
|
||||||
} else if (target.definition.formal.match(query)) {
|
|
||||||
return true;
|
|
||||||
} else if (target.definition.text.resolved.match(query)) {
|
|
||||||
return true;
|
|
||||||
} else if (target.convention.match(query)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TERM) &&
|
||||||
|
target.term.resolved.match(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.EXPR) &&
|
||||||
|
target.definition.formal.match(query)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.TEXT)) {
|
||||||
|
return (target.definition.text.resolved.match(query) || target.convention.match(query));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchRSFormMeta(query: string, target: IRSFormMeta) {
|
export function matchRSFormMeta(query: string, target: IRSFormMeta) {
|
||||||
|
@ -350,3 +377,21 @@ export function matchRSFormMeta(query: string, target: IRSFormMeta) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyGraphFilter(schema: IRSForm, start: number, mode: DependencyMode): IConstituenta[] {
|
||||||
|
if (mode === DependencyMode.ALL) {
|
||||||
|
return schema.items;
|
||||||
|
}
|
||||||
|
let ids: number[] | undefined = undefined
|
||||||
|
switch (mode) {
|
||||||
|
case DependencyMode.OUTPUTS: { ids = schema.graph.nodes.get(start)?.outputs; break; }
|
||||||
|
case DependencyMode.INPUTS: { ids = schema.graph.nodes.get(start)?.inputs; break; }
|
||||||
|
case DependencyMode.EXPAND_OUTPUTS: { ids = schema.graph.expandOutputs([start]) ; break; }
|
||||||
|
case DependencyMode.EXPAND_INPUTS: { ids = schema.graph.expandInputs([start]) ; break; }
|
||||||
|
}
|
||||||
|
if (!ids) {
|
||||||
|
return schema.items;
|
||||||
|
} else {
|
||||||
|
return schema.items.filter(cst => ids!.find(id => id === cst.id));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
import { LayoutTypes } from 'reagraph';
|
import { LayoutTypes } from 'reagraph';
|
||||||
|
|
||||||
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
|
import { resolveErrorClass,RSErrorClass, RSErrorType, TokenID } from './enums';
|
||||||
import { CstType, ExpressionStatus, type IConstituenta, IRSErrorDescription,type IRSForm, ISyntaxTreeNode,ParsingStatus, ValueClass } from './models';
|
import { CstMatchMode, CstType, DependencyMode,ExpressionStatus, IConstituenta,
|
||||||
|
IFunctionArg,IRSErrorDescription, IRSForm,
|
||||||
|
ISyntaxTreeNode, ParsingStatus, ValueClass
|
||||||
|
} from './models';
|
||||||
|
|
||||||
export interface IRSButtonData {
|
export interface IRSButtonData {
|
||||||
text: string
|
text: string
|
||||||
|
@ -14,16 +17,6 @@ export interface IStatusInfo {
|
||||||
tooltip: string
|
tooltip: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTypeLabel(cst: IConstituenta): string {
|
|
||||||
if (cst.parse?.typification) {
|
|
||||||
return cst.parse.typification;
|
|
||||||
}
|
|
||||||
if (cst.parse?.status !== ParsingStatus.VERIFIED) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
return 'Логический';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCstDescription(cst: IConstituenta): string {
|
export function getCstDescription(cst: IConstituenta): string {
|
||||||
if (cst.cstType === CstType.STRUCTURED) {
|
if (cst.cstType === CstType.STRUCTURED) {
|
||||||
return (
|
return (
|
||||||
|
@ -273,6 +266,27 @@ export const mapLayoutLabels: Map<string, string> = new Map([
|
||||||
['nooverlap', 'Без перекрытия']
|
['nooverlap', 'Без перекрытия']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export function getCstCompareLabel(mode: CstMatchMode): string {
|
||||||
|
switch(mode) {
|
||||||
|
case CstMatchMode.ALL: return 'везде';
|
||||||
|
case CstMatchMode.EXPR: return 'ФВ';
|
||||||
|
case CstMatchMode.TERM: return 'термин';
|
||||||
|
case CstMatchMode.TEXT: return 'текст';
|
||||||
|
case CstMatchMode.NAME: return 'ID';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDependencyLabel(mode: DependencyMode): string {
|
||||||
|
switch(mode) {
|
||||||
|
case DependencyMode.ALL: return 'вся схема';
|
||||||
|
case DependencyMode.EXPRESSION: return 'выражение';
|
||||||
|
case DependencyMode.OUTPUTS: return 'потребители';
|
||||||
|
case DependencyMode.INPUTS: return 'поставщики';
|
||||||
|
case DependencyMode.EXPAND_INPUTS: return 'влияющие';
|
||||||
|
case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
|
export const GraphLayoutSelector: {value: LayoutTypes, label: string}[] = [
|
||||||
{ value: 'forceatlas2', label: 'Атлас 2D'},
|
{ value: 'forceatlas2', label: 'Атлас 2D'},
|
||||||
{ value: 'forceDirected2d', label: 'Силы 2D'},
|
{ value: 'forceDirected2d', label: 'Силы 2D'},
|
||||||
|
@ -360,7 +374,8 @@ export function getMockConstituenta(id: number, alias: string, type: CstType, co
|
||||||
status: ParsingStatus.INCORRECT,
|
status: ParsingStatus.INCORRECT,
|
||||||
valueClass: ValueClass.INVALID,
|
valueClass: ValueClass.INVALID,
|
||||||
typification: 'N/A',
|
typification: 'N/A',
|
||||||
syntaxTree: ''
|
syntaxTree: '',
|
||||||
|
args: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -373,6 +388,32 @@ export function getCloneTitle(schema: IRSForm): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTypificationLabel({isValid, resultType, args}: {
|
||||||
|
isValid: boolean,
|
||||||
|
resultType: string,
|
||||||
|
args: IFunctionArg[]
|
||||||
|
}): string {
|
||||||
|
if (!isValid) {
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
if (resultType === '') {
|
||||||
|
resultType = 'Логический'
|
||||||
|
}
|
||||||
|
if (args.length === 0) {
|
||||||
|
return resultType;
|
||||||
|
}
|
||||||
|
const argsText = args.map(arg => arg.typification).join(', ');
|
||||||
|
return `${resultType} 🠔 [${argsText}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCstTypificationLabel(cst: IConstituenta): string {
|
||||||
|
return getTypificationLabel({
|
||||||
|
isValid: cst.parse.status === ParsingStatus.VERIFIED,
|
||||||
|
resultType: cst.parse.typification,
|
||||||
|
args: cst.parse.args
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getRSErrorPrefix(error: IRSErrorDescription): string {
|
export function getRSErrorPrefix(error: IRSErrorDescription): string {
|
||||||
const id = error.errorType.toString(16)
|
const id = error.errorType.toString(16)
|
||||||
switch(resolveErrorClass(error.errorType)) {
|
switch(resolveErrorClass(error.errorType)) {
|
||||||
|
|
|
@ -7,3 +7,11 @@ export function assertIsNode(e: EventTarget | null): asserts e is Node {
|
||||||
export async function delay(ms: number) {
|
export async function delay(ms: number) {
|
||||||
return await new Promise(resolve => setTimeout(resolve, ms));
|
return await new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function trimString(target: string, maxLen: number): string {
|
||||||
|
if (target.length < maxLen) {
|
||||||
|
return target;
|
||||||
|
} else {
|
||||||
|
return target.substring(0, maxLen) + '...';
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user