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
|
||||
docker-compose.yml
|
||||
docker-compose-dev.yml
|
||||
docker-compose-prod.yml
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
# SECURITY SENSITIVE FILES
|
||||
# persistent/*
|
||||
secrets/
|
||||
cert/
|
||||
|
||||
# External distributions
|
||||
rsconcept/backend/import/*.whl
|
||||
|
|
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
|
@ -52,6 +52,26 @@
|
|||
"request": "launch",
|
||||
"script": "${workspaceFolder}/rsconcept/RunServer.ps1",
|
||||
"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 'npm install' in rsconcept\frontend
|
||||
- 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]
|
||||
<details>
|
||||
|
|
19
TODO.txt
19
TODO.txt
|
@ -2,6 +2,23 @@
|
|||
This list only contains global tech refactorings and tech debt
|
||||
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 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:
|
||||
postgres_volume:
|
||||
name: "postgres-db"
|
||||
name: "postgresql-db"
|
||||
django_static_volume:
|
||||
name: "static"
|
||||
django_media_volume:
|
||||
|
@ -12,6 +10,12 @@ networks:
|
|||
default:
|
||||
name: concept-api-net
|
||||
|
||||
secrets:
|
||||
django_key:
|
||||
file: ./secrets/django_key.txt
|
||||
db_password:
|
||||
file: ./secrets/db_password.txt
|
||||
|
||||
services:
|
||||
frontend:
|
||||
restart: always
|
||||
|
@ -19,8 +23,8 @@ services:
|
|||
- backend
|
||||
build:
|
||||
context: ./rsconcept/frontend
|
||||
ports:
|
||||
- 3000:3000
|
||||
expose:
|
||||
- 3000
|
||||
command: serve -s /home/node -l 3000
|
||||
|
||||
|
||||
|
@ -28,12 +32,17 @@ services:
|
|||
restart: always
|
||||
depends_on:
|
||||
- postgresql-db
|
||||
- nginx
|
||||
secrets:
|
||||
- db_password
|
||||
- django_key
|
||||
build:
|
||||
context: ./rsconcept/backend
|
||||
env_file: ./rsconcept/backend/.env.dev
|
||||
ports:
|
||||
- 8000:8000
|
||||
env_file: ./rsconcept/backend/.env.prod
|
||||
environment:
|
||||
SECRET_KEY: /run/secrets/django_key
|
||||
DB_PASSWORD: /run/secrets/db_password
|
||||
expose:
|
||||
- 8000
|
||||
volumes:
|
||||
- django_static_volume:/home/app/web/static
|
||||
- django_media_volume:/home/app/web/media
|
||||
|
@ -44,7 +53,11 @@ services:
|
|||
postgresql-db:
|
||||
restart: always
|
||||
image: postgres:alpine
|
||||
env_file: ./postgresql/.env.dev
|
||||
secrets:
|
||||
- db_password
|
||||
env_file: ./postgresql/.env.prod
|
||||
environment:
|
||||
POSTGRES_PASSWORD: /run/secrets/db_password
|
||||
volumes:
|
||||
- postgres_volume:/var/lib/postgresql/data
|
||||
|
||||
|
@ -54,9 +67,11 @@ services:
|
|||
build:
|
||||
context: ./nginx
|
||||
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;\"'"
|
||||
volumes:
|
||||
- django_static_volume:/var/www/static
|
||||
- django_media_volume:/var/www/media
|
||||
|
|
@ -2,3 +2,4 @@ FROM nginx:stable-alpine3.17-slim
|
|||
|
||||
# Сopу nginx configuration to the proxy-server
|
||||
COPY ./default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./cert/* /etc/ssl/private/
|
|
@ -1,12 +1,17 @@
|
|||
upstream innerdjango {
|
||||
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 {
|
||||
listen 80;
|
||||
server_name rs.acconcept.ru;
|
||||
listen 8000 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 api.portal.acconcept.ru www.api.portal.acconcept.ru;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
|
@ -20,3 +25,17 @@ server {
|
|||
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
|
|
@ -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
|
||||
|
||||
# Copy application sources and setup permissions
|
||||
COPY apps/ ./apps/
|
||||
COPY apps/ ./apps
|
||||
COPY project/ ./project
|
||||
COPY data/ ./data
|
||||
COPY manage.py entrypoint.sh ./
|
||||
RUN sed -i 's/\r$//g' $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/media
|
||||
|
||||
RUN
|
||||
|
||||
USER app
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
|
|
|
@ -43,9 +43,15 @@ class ConstituentaSerializer(serializers.ModelSerializer):
|
|||
fields = '__all__'
|
||||
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()
|
||||
return super().update(instance, validated_data)
|
||||
return result
|
||||
|
||||
|
||||
class StandaloneCstSerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -34,6 +34,7 @@ class TestIntegrations(TestCase):
|
|||
schema = self._default_schema()
|
||||
out1 = json.loads(pc.check_expression(schema, 'X1=X1'))
|
||||
self.assertTrue(out1['parseResult'])
|
||||
self.assertEqual(len(out1['args']), 0)
|
||||
|
||||
out2 = json.loads(pc.check_expression(schema, 'X1=X2'))
|
||||
self.assertFalse(out2['parseResult'])
|
||||
|
|
|
@ -31,6 +31,10 @@ class TestConstituentaAPI(APITestCase):
|
|||
alias='X1', schema=self.rsform_owned, order=1, convention='Test')
|
||||
self.cst2 = Constituenta.objects.create(
|
||||
alias='X2', schema=self.rsform_unowned, order=1, convention='Test1')
|
||||
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):
|
||||
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')
|
||||
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):
|
||||
data = json.dumps({'alias': 'X33', 'order': 10})
|
||||
response = self.client.patch(f'/api/constituents/{self.cst1.id}/', data, content_type='application/json')
|
||||
|
|
|
@ -11,6 +11,7 @@ then
|
|||
echo "Ready!"
|
||||
fi
|
||||
|
||||
cd $APP_HOME
|
||||
python $APP_HOME/manage.py collectstatic --noinput --clear
|
||||
python $APP_HOME/manage.py migrate
|
||||
|
||||
|
|
|
@ -82,9 +82,9 @@ MIDDLEWARE = [
|
|||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
LOGIN_URL = '/accounts/login/'
|
||||
LOGIN_REDIRECT_URL = '/home'
|
||||
LOGOUT_REDIRECT_URL = '/home'
|
||||
LOGIN_URL = '/admin/login/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_REDIRECT_URL = '/'
|
||||
|
||||
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 ./ ./
|
||||
RUN npm install
|
||||
ENV NODE_ENV production
|
||||
RUN npm run build
|
||||
|
||||
# ========= Server =======
|
||||
|
|
4
rsconcept/frontend/package-lock.json
generated
4
rsconcept/frontend/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import Footer from './components/Footer';
|
||||
|
@ -16,9 +17,21 @@ import RSFormPage from './pages/RSFormPage';
|
|||
import UserProfilePage from './pages/UserProfilePage';
|
||||
|
||||
function App () {
|
||||
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 { noNavigation } = useConceptTheme();
|
||||
|
||||
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 (
|
||||
<div className='antialiased clr-app'>
|
||||
<Navigation />
|
||||
|
@ -28,7 +41,9 @@ function App () {
|
|||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<main className={main_clsN}>
|
||||
|
||||
<div className={`${scrollWindowSize} overflow-auto`}>
|
||||
<main className={`${mainSize} px-2`}>
|
||||
<Routes>
|
||||
<Route path='/' element={ <HomePage/>} />
|
||||
|
||||
|
@ -47,6 +62,7 @@ function App () {
|
|||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,9 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
|
|||
function Button({
|
||||
id, text, icon, tooltip,
|
||||
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,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
|
|
|
@ -6,14 +6,15 @@ import Button from './Button';
|
|||
interface ModalProps {
|
||||
title?: string
|
||||
submitText?: string
|
||||
readonly?: boolean
|
||||
canSubmit?: boolean
|
||||
hideWindow: () => void
|
||||
onSubmit: () => void
|
||||
onSubmit?: () => void
|
||||
onCancel?: () => void
|
||||
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);
|
||||
useEscapeKey(hideWindow);
|
||||
|
||||
|
@ -24,29 +25,32 @@ function Modal({ title, hideWindow, onSubmit, onCancel, canSubmit, children, sub
|
|||
|
||||
const handleSubmit = () => {
|
||||
hideWindow();
|
||||
onSubmit();
|
||||
if (onSubmit) onSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='fixed top-0 left-0 z-50 w-full h-full opacity-50 clr-modal'>
|
||||
</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> }
|
||||
<div>
|
||||
<div className='max-h-[calc(95vh-15rem)] overflow-auto'>
|
||||
{children}
|
||||
</div>
|
||||
<div className='flex justify-between w-full pt-4 mt-2 border-t-4'>
|
||||
<Button
|
||||
<div className='flex justify-center w-full gap-4 pt-4 mt-2 border-t-4'>
|
||||
{!readonly && <Button
|
||||
text={submitText}
|
||||
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
||||
colorClass='clr-btn-primary'
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
autoFocus
|
||||
/>
|
||||
/>}
|
||||
<Button
|
||||
text='Отмена'
|
||||
text={readonly ? 'Закрыть' : 'Отмена'}
|
||||
widthClass='min-w-[6rem] min-h-[2.6rem] w-fit h-fit'
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,7 @@ interface SubmitButtonProps {
|
|||
function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: SubmitButtonProps) {
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
{icon && <span>{icon}</span>}
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
import { TextareaHTMLAttributes } from 'react';
|
||||
|
||||
import Label from './Label';
|
||||
|
||||
interface TextAreaProps {
|
||||
id: string
|
||||
interface TextAreaProps
|
||||
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
|
||||
label: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
spellCheck?: boolean
|
||||
placeholder?: string
|
||||
widthClass?: string
|
||||
rows?: number
|
||||
value?: string | ReadonlyArray<string> | number | undefined;
|
||||
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onFocus?: () => void
|
||||
colorClass?: string
|
||||
}
|
||||
|
||||
function TextArea({
|
||||
id, label, placeholder,
|
||||
required, spellCheck, disabled,
|
||||
widthClass = 'w-full', rows = 4, value,
|
||||
onChange, onFocus
|
||||
id, label, required,
|
||||
widthClass = 'w-full',
|
||||
colorClass = 'colorClass',
|
||||
rows = 4,
|
||||
...props
|
||||
}: TextAreaProps) {
|
||||
return (
|
||||
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'>
|
||||
|
@ -28,15 +24,10 @@ function TextArea({
|
|||
htmlFor={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}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
disabled={disabled}
|
||||
spellCheck={spellCheck}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,10 +7,13 @@ interface TextInputProps
|
|||
id: string
|
||||
label: string
|
||||
widthClass?: string
|
||||
colorClass?: string
|
||||
}
|
||||
|
||||
function TextInput({
|
||||
id, required, label, widthClass = 'w-full',
|
||||
id, required, label,
|
||||
widthClass = 'w-full',
|
||||
colorClass = 'clr-input',
|
||||
...props
|
||||
}: TextInputProps) {
|
||||
return (
|
||||
|
@ -21,7 +24,7 @@ function TextInput({
|
|||
htmlFor={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}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { urls } from '../utils/constants';
|
||||
import { GithubIcon } from './Icons';
|
||||
|
||||
function Footer() {
|
||||
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='px-4 underline'>
|
||||
<div className='px-4 underline whitespace-nowrap'>
|
||||
<Link to='/manuals' 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 className='px-4 underline border-gray-400 border-x dark:border-gray-300'>
|
||||
<ul>
|
||||
|
@ -23,9 +28,9 @@ function Footer() {
|
|||
</li>
|
||||
</ul>
|
||||
</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>Данный инструмент работы с экспликациями концептуальных схем в родоструктурной форме является уникальной Российской разработкой и вобрал в себя разработки начиная с 1990-х годов</p>
|
||||
<p>Портал позволяет анализировать предметные области, формально записывать системы определений и синтезировать их с помощью математического аппарата родов структур</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer >
|
||||
|
|
|
@ -292,3 +292,11 @@ export function HelpIcon(props: IconProps) {
|
|||
</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') };
|
||||
|
||||
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 &&
|
||||
<button
|
||||
title='Скрыть навигацию'
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons';
|
||||
import NavigationButton from './NavigationButton';
|
||||
|
||||
function UserTools() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const navigateCreateRSForm = () => { navigate('/rsform-create'); };
|
||||
const navigateMyWork = () => { navigate('/library?filter=personal'); };
|
||||
|
@ -24,7 +26,7 @@ function UserTools() {
|
|||
/>
|
||||
</span>
|
||||
<NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} />
|
||||
<NavigationButton icon={<BellIcon />} description='Уведомления' onClick={handleNotifications} />
|
||||
{ user && user.is_staff && <NavigationButton icon={<BellIcon />} description='Уведомления' onClick={handleNotifications} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useAuth } from '../context/AuthContext';
|
||||
import TextURL from './Common/TextURL';
|
||||
import InfoMessage from './InfoMessage';
|
||||
|
||||
interface RequireAuthProps {
|
||||
children: React.ReactNode
|
||||
|
@ -13,12 +12,12 @@ function RequireAuth({ children }: RequireAuthProps) {
|
|||
<>
|
||||
{user && children}
|
||||
{!user &&
|
||||
<div className='flex flex-col items-center'>
|
||||
<InfoMessage message={'Данная функция доступна только зарегистрированным пользователям. Пожалуйста войдите в систему'} />
|
||||
<div className='flex flex-col items-start'>
|
||||
<TextURL text='Войти в систему...' href='login' />
|
||||
<TextURL text='Зарегистрироваться...' href='signup' />
|
||||
</div>
|
||||
<div className='flex flex-col items-center mt-2 gap-1'>
|
||||
<p><b>Данная страница доступна только зарегистрированным пользователям</b></p>
|
||||
<p className='mb-2'>Пожалуйста войдите в систему</p>
|
||||
<TextURL text='Войти в систему' href='/login'/>
|
||||
<TextURL text='Зарегистрироваться' href='/signup'/>
|
||||
<TextURL text='Начальная страница' href='/'/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
|
|
|
@ -10,6 +10,7 @@ interface IAuthContext {
|
|||
login: (data: IUserLoginData, callback?: DataCallback) => void
|
||||
logout: (callback?: DataCallback) => void
|
||||
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void
|
||||
reload: (callback?: () => void) => void
|
||||
loading: boolean
|
||||
error: ErrorInfo
|
||||
setError: (error: ErrorInfo) => void
|
||||
|
@ -48,8 +49,7 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
|||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [setUser]
|
||||
);
|
||||
}, [setUser]);
|
||||
|
||||
function login(data: IUserLoginData, callback?: DataCallback) {
|
||||
setError(undefined);
|
||||
|
@ -87,7 +87,7 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
|||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ user, login, logout, signup, loading, error, setError }}
|
||||
value={{ user, login, logout, signup, loading, error, reload, setError }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
import { getLibrary } from '../utils/backendAPI';
|
||||
import { ILibraryFilter, IRSFormMeta, matchRSFormMeta } from '../utils/models';
|
||||
import { DataCallback, getLibrary, postNewRSForm } from '../utils/backendAPI';
|
||||
import { ILibraryFilter, IRSFormCreateData, IRSFormMeta, matchRSFormMeta } from '../utils/models';
|
||||
|
||||
interface ILibraryContext {
|
||||
items: IRSFormMeta[]
|
||||
loading: boolean
|
||||
processing: boolean
|
||||
error: ErrorInfo
|
||||
setError: (error: ErrorInfo) => void
|
||||
|
||||
reload: () => void
|
||||
filter: (params: ILibraryFilter) => IRSFormMeta[]
|
||||
createSchema: (data: IRSFormCreateData, callback?: DataCallback<IRSFormMeta>) => void
|
||||
}
|
||||
|
||||
const LibraryContext = createContext<ILibraryContext | null>(null)
|
||||
|
@ -32,6 +34,7 @@ interface LibraryStateProps {
|
|||
export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||
const [ items, setItems ] = useState<IRSFormMeta[]>([])
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ processing, setProcessing ] = useState(false);
|
||||
const [ error, setError ] = useState<ErrorInfo>(undefined);
|
||||
|
||||
const filter = useCallback(
|
||||
|
@ -63,12 +66,27 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<LibraryContext.Provider value={{
|
||||
items, loading, error, setError,
|
||||
reload, filter
|
||||
items, loading, processing, error, setError,
|
||||
reload, filter, createSchema
|
||||
}}>
|
||||
{ children }
|
||||
</LibraryContext.Provider>
|
||||
|
|
|
@ -5,14 +5,17 @@ import { type ErrorInfo } from '../components/BackendError'
|
|||
import { useRSFormDetails } from '../hooks/useRSFormDetails'
|
||||
import {
|
||||
type DataCallback, deleteRSForm, getTRSFile,
|
||||
patchConstituenta, patchDeleteConstituenta, patchMoveConstituenta, patchResetAliases,
|
||||
patchRSForm,
|
||||
patchUploadTRS, postClaimRSForm, postCloneRSForm,postNewConstituenta} from '../utils/backendAPI'
|
||||
patchConstituenta, patchDeleteConstituenta,
|
||||
patchMoveConstituenta, patchResetAliases, patchRSForm,
|
||||
patchUploadTRS, postClaimRSForm, postCloneRSForm, postNewConstituenta
|
||||
} from '../utils/backendAPI'
|
||||
import {
|
||||
IConstituentaList, IConstituentaMeta, ICstCreateData,
|
||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData, IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||
ICstMovetoData, ICstUpdateData, IRSForm, IRSFormCreateData,
|
||||
IRSFormData, IRSFormMeta, IRSFormUpdateData, IRSFormUploadData
|
||||
} from '../utils/models'
|
||||
import { useAuth } from './AuthContext'
|
||||
import { useLibrary } from './LibraryContext'
|
||||
|
||||
interface IRSFormContext {
|
||||
schema?: IRSForm
|
||||
|
@ -63,15 +66,16 @@ interface RSFormStateProps {
|
|||
}
|
||||
|
||||
export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
||||
const { user } = useAuth()
|
||||
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID })
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const { user } = useAuth();
|
||||
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
|
||||
const [ processing, setProcessing ] = useState(false);
|
||||
const library = useLibrary();
|
||||
|
||||
const [isForceAdmin, setIsForceAdmin] = useState(false)
|
||||
const [isReadonly, setIsReadonly] = useState(false)
|
||||
const [ isForceAdmin, setIsForceAdmin ] = useState(false);
|
||||
const [ isReadonly, setIsReadonly ] = useState(false);
|
||||
|
||||
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema?.owner])
|
||||
const isClaimable = 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 isEditable = useMemo(
|
||||
() => {
|
||||
return (
|
||||
|
@ -83,12 +87,12 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
const isTracking = useMemo(
|
||||
() => {
|
||||
return true
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const toggleTracking = useCallback(
|
||||
() => {
|
||||
toast('not implemented yet')
|
||||
}, [])
|
||||
toast.info('Отслеживание в разработке...')
|
||||
}, []);
|
||||
|
||||
const update = useCallback(
|
||||
(data: IRSFormUpdateData, callback?: DataCallback<IRSFormMeta>) => {
|
||||
|
@ -100,13 +104,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onError: error => setError(error),
|
||||
onSuccess: newData => {
|
||||
setSchema(Object.assign(schema, newData));
|
||||
if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, setSchema, schema])
|
||||
}, [schemaID, setError, setSchema, schema]);
|
||||
|
||||
const upload = useCallback(
|
||||
(data: IRSFormUploadData, callback?: () => void) => {
|
||||
|
@ -118,13 +122,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onError: error => setError(error),
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, setSchema, schema])
|
||||
}, [schemaID, setError, setSchema, schema]);
|
||||
|
||||
const destroy = useCallback(
|
||||
(callback?: () => void) => {
|
||||
|
@ -132,13 +136,14 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
deleteRSForm(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onError: error => setError(error),
|
||||
onSuccess: () => {
|
||||
setSchema(undefined);
|
||||
library.reload();
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, setSchema])
|
||||
}, [schemaID, setError, setSchema, library]);
|
||||
|
||||
const claim = useCallback(
|
||||
(callback?: DataCallback<IRSFormMeta>) => {
|
||||
|
@ -149,13 +154,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
postClaimRSForm(schemaID, {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onError: error => setError(error),
|
||||
onSuccess: newData => {
|
||||
setSchema(Object.assign(schema, newData));
|
||||
if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user, setSchema])
|
||||
}, [schemaID, setError, schema, user, setSchema]);
|
||||
|
||||
const clone = useCallback(
|
||||
(data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => {
|
||||
|
@ -167,10 +172,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error) },
|
||||
onSuccess: callback
|
||||
onError: error => setError(error),
|
||||
onSuccess: newSchema => {
|
||||
library.reload();
|
||||
if (callback) callback(newSchema);
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user])
|
||||
}, [schemaID, setError, schema, user, library]);
|
||||
|
||||
const resetAliases = useCallback(
|
||||
(callback?: () => void) => {
|
||||
|
@ -187,7 +195,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
}, [schemaID, setError, schema, user, setSchema])
|
||||
}, [schemaID, setError, schema, user, setSchema]);
|
||||
|
||||
const download = useCallback(
|
||||
(callback: DataCallback<Blob>) => {
|
||||
|
@ -198,7 +206,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => { setError(error) },
|
||||
onSuccess: callback
|
||||
});
|
||||
}, [schemaID, setError])
|
||||
}, [schemaID, setError]);
|
||||
|
||||
const cstCreate = useCallback(
|
||||
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||
|
@ -242,7 +250,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
reload(setProcessing, () => { if (callback != null) callback(newData); })
|
||||
}
|
||||
});
|
||||
}, [setError, reload])
|
||||
}, [setError, reload]);
|
||||
|
||||
const cstMoveTo = useCallback(
|
||||
(data: ICstMovetoData, callback?: () => void) => {
|
||||
|
@ -273,5 +281,5 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
}}>
|
||||
{ children }
|
||||
</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
|
||||
}
|
||||
|
||||
.clr-input {
|
||||
@apply dark:bg-black bg-white disabled:bg-[#f0f2f7] dark:disabled:bg-gray-700
|
||||
}
|
||||
|
||||
.clr-footer {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 */
|
||||
|
|
|
@ -10,12 +10,12 @@ import SubmitButton from '../components/Common/SubmitButton';
|
|||
import TextArea from '../components/Common/TextArea';
|
||||
import TextInput from '../components/Common/TextInput';
|
||||
import RequireAuth from '../components/RequireAuth';
|
||||
import useNewRSForm from '../hooks/useNewRSForm';
|
||||
import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
|
||||
import { useLibrary } from '../context/LibraryContext';
|
||||
import { IRSFormCreateData } from '../utils/models';
|
||||
|
||||
function CreateRSFormPage() {
|
||||
const navigate = useNavigate();
|
||||
const { createSchema, error, setError, loading } = useNewRSForm()
|
||||
const { createSchema, error, setError, processing } = useLibrary();
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [alias, setAlias] = useState('');
|
||||
|
@ -35,14 +35,9 @@ function CreateRSFormPage() {
|
|||
}
|
||||
}
|
||||
|
||||
function onSuccess(newSchema: IRSFormMeta) {
|
||||
toast.success('Схема успешно создана');
|
||||
navigate(`/rsforms/${newSchema.id}`);
|
||||
}
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (loading) {
|
||||
if (processing) {
|
||||
return;
|
||||
}
|
||||
const data: IRSFormCreateData = {
|
||||
|
@ -53,7 +48,10 @@ function CreateRSFormPage() {
|
|||
file: file,
|
||||
fileName: file?.name
|
||||
};
|
||||
createSchema(data, onSuccess);
|
||||
createSchema(data, (newSchema) => {
|
||||
toast.success('Схема успешно создана');
|
||||
navigate(`/rsforms/${newSchema.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -92,7 +90,7 @@ function CreateRSFormPage() {
|
|||
<div className='flex items-center justify-center py-2 mt-4'>
|
||||
<SubmitButton
|
||||
text='Создать схему'
|
||||
loading={loading}
|
||||
loading={processing}
|
||||
/>
|
||||
</div>
|
||||
{ error && <BackendError error={error} />}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useLayoutEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
@ -5,11 +6,14 @@ import { useAuth } from '../context/AuthContext';
|
|||
function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!user) {
|
||||
navigate('/library?filter=common');
|
||||
} else if(!user.is_staff) {
|
||||
navigate('/library?filter=personal');
|
||||
}
|
||||
}, [navigate, user])
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<p>Список схем пуст</p>
|
||||
<p>Измените фильтр или создайти новую концептуальную схему</p>
|
||||
<p>Измените фильтр или создайте новую концептуальную схему</p>
|
||||
</span>}
|
||||
|
||||
pagination
|
||||
|
|
|
@ -6,7 +6,6 @@ import Form from '../components/Common/Form';
|
|||
import SubmitButton from '../components/Common/SubmitButton';
|
||||
import TextInput from '../components/Common/TextInput';
|
||||
import TextURL from '../components/Common/TextURL';
|
||||
import InfoMessage from '../components/InfoMessage';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { IUserLoginData } from '../utils/models';
|
||||
|
||||
|
@ -41,7 +40,7 @@ function LoginPage() {
|
|||
|
||||
return (
|
||||
<div className='w-full py-2'> { user
|
||||
? <InfoMessage message={`Вы вошли в систему как ${user.username}`} />
|
||||
? <b>{`Вы вошли в систему как ${user.username}`}</b>
|
||||
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'>
|
||||
<TextInput id='username'
|
||||
label='Имя пользователя'
|
||||
|
|
|
@ -16,10 +16,6 @@ interface DlgShowASTProps {
|
|||
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||
const { darkMode } = useConceptTheme();
|
||||
|
||||
function handleSubmit() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
const nodes: GraphNode[] = useMemo(
|
||||
() => syntaxTree.map(node => {
|
||||
return {
|
||||
|
@ -46,14 +42,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
return (
|
||||
<Modal
|
||||
hideWindow={hideWindow}
|
||||
onSubmit={handleSubmit}
|
||||
submitText='Закрыть'
|
||||
canSubmit={true}
|
||||
readonly
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<div className='w-full text-lg text-center'>{expression}</div>
|
||||
<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
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||
import Divider from '../../components/Common/Divider';
|
||||
import MiniButton from '../../components/Common/MiniButton';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
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 { 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 ViewSideConstituents from './elements/ViewSideConstituents';
|
||||
|
||||
|
@ -62,9 +64,11 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
|||
setTerm(activeCst.term?.raw ?? '');
|
||||
setTextDefinition(activeCst.definition?.text?.raw ?? '');
|
||||
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>) {
|
||||
event.preventDefault();
|
||||
|
@ -105,12 +109,12 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-start w-full gap-2'>
|
||||
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min px-4 py-2 border'>
|
||||
<div className='flex items-stretch w-full gap-2 mb-2'>
|
||||
<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'>
|
||||
<button type='submit'
|
||||
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}
|
||||
>
|
||||
<SaveIcon size={5} />
|
||||
|
@ -150,6 +154,36 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
|||
onClick={handleDelete}
|
||||
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>
|
||||
<TextArea id='term' label='Термин'
|
||||
|
@ -167,6 +201,7 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
|||
disabled
|
||||
/>
|
||||
<EditorRSExpression id='expression' label='Формальное выражение'
|
||||
activeCst={activeCst}
|
||||
placeholder='Родоструктурное выражение, задающее формальное определение'
|
||||
value={expression}
|
||||
disabled={!isEnabled}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useRSForm } from '../../context/RSFormContext';
|
|||
import { useConceptTheme } from '../../context/ThemeContext';
|
||||
import { prefixes } from '../../utils/constants';
|
||||
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 {
|
||||
onOpenEdit: (cstID: number) => void
|
||||
|
@ -192,10 +192,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
{
|
||||
name: 'Тип',
|
||||
id: 'type',
|
||||
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getTypeLabel(cst)}</div>,
|
||||
width: '140px',
|
||||
minWidth: '100px',
|
||||
maxWidth: '140px',
|
||||
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getCstTypificationLabel(cst)}</div>,
|
||||
width: '175px',
|
||||
maxWidth: '175px',
|
||||
wrap: true,
|
||||
reorder: true,
|
||||
hide: 1600
|
||||
|
@ -249,7 +248,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
<div className='w-full'>
|
||||
<div
|
||||
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'>
|
||||
Выбраны
|
||||
|
@ -257,25 +256,25 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0}
|
||||
</span>
|
||||
</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
|
||||
tooltip='Переместить вверх'
|
||||
icon={<ArrowUpIcon size={6}/>}
|
||||
disabled={nothingSelected}
|
||||
disabled={!isEditable || nothingSelected}
|
||||
dense
|
||||
onClick={handleMoveUp}
|
||||
/>
|
||||
<Button
|
||||
tooltip='Переместить вниз'
|
||||
icon={<ArrowDownIcon size={6}/>}
|
||||
disabled={nothingSelected}
|
||||
disabled={!isEditable || nothingSelected}
|
||||
dense
|
||||
onClick={handleMoveDown}
|
||||
/>
|
||||
<Button
|
||||
tooltip='Удалить выбранные'
|
||||
icon={<DumpBinIcon color={!nothingSelected ? 'text-red' : ''} size={6}/>}
|
||||
disabled={nothingSelected}
|
||||
disabled={!isEditable || nothingSelected}
|
||||
dense
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
|
@ -284,12 +283,14 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
tooltip='Переиндексировать имена'
|
||||
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
|
||||
dense
|
||||
disabled={!isEditable}
|
||||
onClick={handleReindex}
|
||||
/>
|
||||
<Button
|
||||
tooltip='Новая конституента'
|
||||
icon={<SmallPlusIcon color='text-green' size={6}/>}
|
||||
dense
|
||||
disabled={!isEditable}
|
||||
onClick={() => handleCreateCst()}
|
||||
/>
|
||||
{(Object.values(CstType)).map(
|
||||
|
@ -299,6 +300,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
text={`${getCstTypePrefix(type)}`}
|
||||
tooltip={getCstTypeShortcut(type)}
|
||||
dense
|
||||
disabled={!isEditable}
|
||||
onClick={() => handleCreateCst(type)}
|
||||
/>;
|
||||
})}
|
||||
|
@ -328,7 +330,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
|||
})}
|
||||
</div>
|
||||
</ConceptTooltip>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full h-full' onKeyDown={handleTableKey}>
|
||||
<ConceptDataTable
|
||||
|
|
|
@ -7,8 +7,8 @@ import { Loader } from '../../components/Common/Loader';
|
|||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import useCheckExpression from '../../hooks/useCheckExpression';
|
||||
import { TokenID } from '../../utils/enums';
|
||||
import { IRSErrorDescription, SyntaxTree } from '../../utils/models';
|
||||
import { getCstExpressionPrefix } from '../../utils/staticUI';
|
||||
import { IConstituenta, IRSErrorDescription, SyntaxTree } from '../../utils/models';
|
||||
import { getCstExpressionPrefix, getTypificationLabel } from '../../utils/staticUI';
|
||||
import ParsingResult from './elements/ParsingResult';
|
||||
import RSLocalButton from './elements/RSLocalButton';
|
||||
import RSTokenButton from './elements/RSTokenButton';
|
||||
|
@ -17,6 +17,7 @@ import { getSymbolSubstitute, TextWrapper } from './elements/textEditing';
|
|||
|
||||
interface EditorRSExpressionProps {
|
||||
id: string
|
||||
activeCst?: IConstituenta
|
||||
label: string
|
||||
isActive: boolean
|
||||
disabled?: boolean
|
||||
|
@ -30,10 +31,10 @@ interface EditorRSExpressionProps {
|
|||
}
|
||||
|
||||
function EditorRSExpression({
|
||||
id, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
||||
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
||||
toggleEditMode, setTypification, onChange
|
||||
}: EditorRSExpressionProps) {
|
||||
const { schema, activeCst } = useRSForm();
|
||||
const { schema } = useRSForm();
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
|
||||
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
|
||||
|
@ -66,7 +67,11 @@ function EditorRSExpression({
|
|||
}
|
||||
expressionCtrl.current!.focus();
|
||||
setIsModified(false);
|
||||
setTypification(parse.typification);
|
||||
setTypification(getTypificationLabel({
|
||||
isValid: parse.parseResult,
|
||||
resultType: parse.typification,
|
||||
args: parse.args
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -205,13 +210,22 @@ function EditorRSExpression({
|
|||
|
||||
return (
|
||||
<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
|
||||
text={label}
|
||||
required={false}
|
||||
htmlFor={id}
|
||||
/>
|
||||
<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}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
|
@ -223,23 +237,15 @@ function EditorRSExpression({
|
|||
/>
|
||||
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{isActive && <StatusBar
|
||||
isModified={isModified}
|
||||
constituenta={activeCst}
|
||||
parseData={parseData}
|
||||
/>}
|
||||
<Button
|
||||
tooltip='Проверить формальное выражение'
|
||||
text='Проверить'
|
||||
widthClass='h-full w-fit'
|
||||
colorClass='clr-btn-default'
|
||||
onClick={handleCheckExpression}
|
||||
/>
|
||||
</div>
|
||||
{isActive && EditButtons}
|
||||
{!isActive && <StatusBar
|
||||
isModified={isModified}
|
||||
constituenta={activeCst}
|
||||
parseData={parseData}
|
||||
/>}
|
||||
</div>
|
||||
{ (loading || parseData) &&
|
||||
<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 { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, useSelection } from 'reagraph';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { darkTheme, GraphCanvas, GraphCanvasRef, GraphEdge, GraphNode, LayoutTypes, lightTheme, Sphere, useSelection } from 'reagraph';
|
||||
|
||||
import Button from '../../components/Common/Button';
|
||||
import Checkbox from '../../components/Common/Checkbox';
|
||||
import ConceptSelect from '../../components/Common/ConceptSelect';
|
||||
import { ArrowsRotateIcon } from '../../components/Icons';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import { useConceptTheme } from '../../context/ThemeContext';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import { resources } from '../../utils/constants';
|
||||
import { Graph } from '../../utils/Graph';
|
||||
import { GraphLayoutSelector,mapLayoutLabels } from '../../utils/staticUI';
|
||||
|
||||
function EditorTermGraph() {
|
||||
const { schema } = useRSForm();
|
||||
const { darkMode } = useConceptTheme();
|
||||
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'forceatlas2');
|
||||
const { darkMode, noNavigation } = useConceptTheme();
|
||||
const [ layout, setLayout ] = useLocalStorage<LayoutTypes>('graph_layout', 'treeTd2d');
|
||||
|
||||
const [ filtered, setFiltered ] = useState<Graph>(new Graph());
|
||||
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);
|
||||
|
||||
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(() => {
|
||||
return schema?.items.map(cst => {
|
||||
return {
|
||||
id: String(cst.id),
|
||||
label: (cst.term.resolved || cst.term.raw) ? `${cst.alias}: ${cst.term.resolved || cst.term.raw}` : cst.alias
|
||||
}}
|
||||
) ?? [];
|
||||
}, [schema?.items]);
|
||||
const result: GraphNode[] = [];
|
||||
if (!schema) {
|
||||
return result;
|
||||
}
|
||||
filtered.nodes.forEach(node => {
|
||||
const cst = schema.items.find(cst => cst.id === node.id);
|
||||
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 result: GraphEdge[] = [];
|
||||
let edgeID = 1;
|
||||
schema?.graph.nodes.forEach(source => {
|
||||
filtered.nodes.forEach(source => {
|
||||
source.outputs.forEach(target => {
|
||||
result.push({
|
||||
id: String(edgeID),
|
||||
|
@ -40,7 +69,7 @@ function EditorTermGraph() {
|
|||
});
|
||||
});
|
||||
return result;
|
||||
}, [schema?.graph]);
|
||||
}, [filtered.nodes]);
|
||||
|
||||
const handleCenter = useCallback(() => {
|
||||
graphRef.current?.resetControls();
|
||||
|
@ -62,29 +91,47 @@ function EditorTermGraph() {
|
|||
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 (<>
|
||||
<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'>
|
||||
<div className='flex items-center gap-1 w-[15rem]'>
|
||||
<Button
|
||||
icon={<ArrowsRotateIcon size={8} />}
|
||||
dense
|
||||
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
|
||||
label='Анимация вращения'
|
||||
widthClass='w-full'
|
||||
value={orbit}
|
||||
onChange={ event => setOrbit(event.target.checked) }/>
|
||||
<Button
|
||||
text='Центрировать'
|
||||
dense
|
||||
onClick={handleCenter}
|
||||
onChange={ event => setOrbit(event.target.checked) }
|
||||
/>
|
||||
<Checkbox
|
||||
label='Удалить несвязанные'
|
||||
value={noHermits}
|
||||
onChange={ event => setNoHermits(event.target.checked) }
|
||||
/>
|
||||
<Checkbox
|
||||
label='Транзитивная редукция'
|
||||
value={noTransitive}
|
||||
onChange={ event => setNoTransitive(event.target.checked) }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
draggable
|
||||
ref={graphRef}
|
||||
|
@ -99,9 +146,12 @@ function EditorTermGraph() {
|
|||
onNodePointerOver={onNodePointerOver}
|
||||
onNodePointerOut={onNodePointerOut}
|
||||
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}
|
||||
theme={darkMode ? darkTheme : lightTheme}
|
||||
renderNode={({ node, ...rest }) => (
|
||||
<Sphere {...rest} node={node} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,6 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
|||
const schemaMenu = useDropdown();
|
||||
const editMenu = useDropdown();
|
||||
|
||||
|
||||
const handleClaimOwner = useCallback(() => {
|
||||
editMenu.hide();
|
||||
claimOwnershipProc(claim)
|
||||
|
@ -61,12 +60,13 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
|||
}, [schemaMenu]);
|
||||
|
||||
return (
|
||||
<div className='flex items-center w-fit'>
|
||||
<div className='flex items-stretch w-fit'>
|
||||
<div ref={schemaMenu.ref}>
|
||||
<Button
|
||||
tooltip='Действия'
|
||||
icon={<MenuIcon size={5}/>}
|
||||
borderClass=''
|
||||
widthClass='h-full w-fit'
|
||||
dense
|
||||
onClick={schemaMenu.toggle}
|
||||
/>
|
||||
|
@ -108,6 +108,7 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
|||
<Button
|
||||
tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')}
|
||||
borderClass=''
|
||||
widthClass='h-full w-fit'
|
||||
icon={<PenIcon size={5} color={isEditable ? 'text-green' : 'text-red'}/>}
|
||||
dense
|
||||
onClick={editMenu.toggle}
|
||||
|
@ -144,6 +145,7 @@ function RSTabsMenu({showUploadDialog, showCloneDialog}: RSTabsMenuProps) {
|
|||
? <EyeIcon color='text-primary' size={5}/>
|
||||
: <EyeOffIcon size={5}/>
|
||||
}
|
||||
widthClass='h-full w-fit'
|
||||
borderClass=''
|
||||
dense
|
||||
onClick={toggleTracking}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
|
||||
import { IConstituenta } from '../../../utils/models';
|
||||
import { getTypeLabel } from '../../../utils/staticUI';
|
||||
import { getCstTypificationLabel } from '../../../utils/staticUI';
|
||||
|
||||
interface ConstituentaTooltipProps {
|
||||
data: IConstituenta
|
||||
|
@ -14,7 +14,7 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
|
|||
className='max-w-[25rem] min-w-[25rem]'
|
||||
>
|
||||
<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>
|
||||
{data.definition.formal && <p><b>Выражение: </b>{data.definition.formal}</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 LabeledText from '../../../components/Common/LabeledText';
|
||||
import { type IRSFormStats } from '../../../utils/models';
|
||||
|
@ -9,7 +8,7 @@ interface RSFormStatsProps {
|
|||
|
||||
function RSFormStats({ stats }: RSFormStatsProps) {
|
||||
return (
|
||||
<Card>
|
||||
<div className='px-4 py-2 border'>
|
||||
<LabeledText id='count_all'
|
||||
label='Всего конституент '
|
||||
text={stats.count_all}
|
||||
|
@ -28,12 +27,12 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
|||
label='Невычислимы '
|
||||
text={stats.count_incalc}
|
||||
/>}
|
||||
<Divider />
|
||||
<Divider margins='my-1' />
|
||||
<LabeledText id='count_termin'
|
||||
label='Термины '
|
||||
text={stats.count_termin}
|
||||
/>
|
||||
<Divider />
|
||||
<Divider margins='my-1' />
|
||||
{ stats.count_base > 0 &&
|
||||
<LabeledText id='count_base'
|
||||
label='Базисные множества '
|
||||
|
@ -74,7 +73,7 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
|||
label='Теормы '
|
||||
text={stats.count_theorem}
|
||||
/>}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,8 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
|
|||
const data = mapStatusInfo.get(status)!;
|
||||
return (
|
||||
<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}>
|
||||
{data.text}
|
||||
className={`text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center justify-center align-middle ${data.color}`}>
|
||||
Статус: [ {data.text} ]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Checkbox from '../../../components/Common/Checkbox';
|
||||
import ConceptDataTable from '../../../components/Common/ConceptDataTable';
|
||||
import { useRSForm } from '../../../context/RSFormContext';
|
||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||
import useLocalStorage from '../../../hooks/useLocalStorage';
|
||||
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 ConstituentaTooltip from './ConstituentaTooltip';
|
||||
import DependencyModePicker from './DependencyModePicker';
|
||||
import MatchModePicker from './MatchModePicker';
|
||||
|
||||
interface ViewSideConstituentsProps {
|
||||
expression: string
|
||||
|
@ -19,31 +20,38 @@ interface ViewSideConstituentsProps {
|
|||
function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
||||
const { darkMode } = useConceptTheme();
|
||||
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 [filterText, setFilterText] = useLocalStorage('side-filter-text', '')
|
||||
const [onlyExpression, setOnlyExpression] = useLocalStorage('side-filter-flag', false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!schema?.items) {
|
||||
setFilteredData([]);
|
||||
return;
|
||||
}
|
||||
if (onlyExpression) {
|
||||
let filtered: IConstituenta[] = [];
|
||||
if (filterSource === DependencyMode.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 diff = Array.from(aliases).filter(name => !names.includes(name));
|
||||
if (diff.length > 0) {
|
||||
diff.forEach(
|
||||
(alias, index) => filtered.push(getMockConstituenta(-index, alias, CstType.BASE, 'Конституента отсутствует')));
|
||||
}
|
||||
setFilteredData(filtered);
|
||||
} else if (!filterText) {
|
||||
setFilteredData(schema?.items);
|
||||
} else if (!activeID) {
|
||||
filtered = schema.items
|
||||
} 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(
|
||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
|
@ -132,23 +140,17 @@ function ViewSideConstituents({ expression, activeID, onOpenEdit }: ViewSideCons
|
|||
);
|
||||
|
||||
return (
|
||||
<div className='max-h-[80vh] 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='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 rounded clr-bg-pop clr-border'>
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<MatchModePicker value={filterMatch} onChange={setFilterMatch}/>
|
||||
<input type='text'
|
||||
className='w-full px-2 outline-none dark:bg-gray-700 hover:text-clip'
|
||||
placeholder='текст для фильтрации списка'
|
||||
className='w-full px-2 bg-white outline-none hover:text-clip clr-bg-pop clr-border'
|
||||
placeholder='наберите текст фильтра'
|
||||
value={filterText}
|
||||
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>
|
||||
<ConceptDataTable
|
||||
|
|
|
@ -6,7 +6,6 @@ import BackendError from '../components/BackendError';
|
|||
import Form from '../components/Common/Form';
|
||||
import SubmitButton from '../components/Common/SubmitButton';
|
||||
import TextInput from '../components/Common/TextInput';
|
||||
import InfoMessage from '../components/InfoMessage';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { type IUserSignupData } from '../utils/models';
|
||||
|
||||
|
@ -46,7 +45,7 @@ function RegisterPage() {
|
|||
return (
|
||||
<div className='w-full py-2'>
|
||||
{ user &&
|
||||
<InfoMessage message={`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`} /> }
|
||||
<b>{`Вы вошли в систему как ${user.username}. Если хотите зарегистрировать нового пользователя, выйдите из системы (меню в правом верхнем углу экрана)`}</b>}
|
||||
{ !user &&
|
||||
<Form title='Регистрация пользователя' onSubmit={handleSubmit}>
|
||||
<TextInput id='username' label='Имя пользователя' type='text'
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import InfoMessage from '../components/InfoMessage';
|
||||
|
||||
function RestorePasswordPage() {
|
||||
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.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', () => {
|
||||
|
|
|
@ -10,6 +10,13 @@ export class GraphNode {
|
|||
this.inputs = [];
|
||||
}
|
||||
|
||||
clone(): GraphNode {
|
||||
const result = new GraphNode(this.id);
|
||||
result.outputs = [... this.outputs];
|
||||
result.inputs = [... this.inputs];
|
||||
return result;
|
||||
}
|
||||
|
||||
addOutput(node: number): void {
|
||||
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 {
|
||||
let node = this.nodes.get(target);
|
||||
if (!node) {
|
||||
|
@ -67,6 +80,16 @@ export class Graph {
|
|||
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 {
|
||||
const sourceNode = this.addNode(source);
|
||||
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[] {
|
||||
const result: number[] = [];
|
||||
const marked = new Map<number, boolean>();
|
||||
|
@ -143,26 +174,62 @@ export class Graph {
|
|||
return result;
|
||||
}
|
||||
|
||||
visitDFS(visitor: (node: GraphNode) => void) {
|
||||
const visited: Map<number, boolean> = new Map();
|
||||
tolopogicalOrder(): number[] {
|
||||
const result: number[] = [];
|
||||
const marked = new Map<number, boolean>();
|
||||
this.nodes.forEach(node => {
|
||||
if (!visited.has(node.id)) {
|
||||
this.depthFirstSearch(node, visited, visitor);
|
||||
if (marked.get(node.id)) {
|
||||
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(
|
||||
node: GraphNode,
|
||||
visited: Map<number, boolean>,
|
||||
visitor: (node: GraphNode) => void)
|
||||
: void {
|
||||
visited.set(node.id, true);
|
||||
visitor(node);
|
||||
node.outputs.forEach((item) => {
|
||||
if (!visited.has(item)) {
|
||||
const childNode = this.nodes.get(item)!;
|
||||
this.depthFirstSearch(childNode, visited, visitor);
|
||||
transitiveReduction() {
|
||||
const order = this.tolopogicalOrder();
|
||||
const marked = new Map<number, boolean>();
|
||||
order.forEach(nodeID => {
|
||||
if (marked.get(nodeID)) {
|
||||
return;
|
||||
}
|
||||
const stack: {id: number, parents: number[]}[] = [];
|
||||
stack.push({id: nodeID, parents: []});
|
||||
while (stack.length > 0) {
|
||||
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
|
||||
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 = {
|
||||
|
@ -16,7 +18,8 @@ export const urls = {
|
|||
exteor64: 'https://drive.google.com/open?id=1IJt25ZRQ-ZMA6t7hOqmo5cv05WJCQKMv&usp=drive_fs',
|
||||
ponomarev: 'https://inponomarev.ru/textbook',
|
||||
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 = {
|
||||
|
|
|
@ -69,6 +69,11 @@ export interface ISyntaxTreeNode {
|
|||
}
|
||||
export type SyntaxTree = ISyntaxTreeNode[]
|
||||
|
||||
export interface IFunctionArg {
|
||||
alias: string
|
||||
typification: string
|
||||
}
|
||||
|
||||
export interface IExpressionParse {
|
||||
parseResult: boolean
|
||||
syntax: Syntax
|
||||
|
@ -77,6 +82,7 @@ export interface IExpressionParse {
|
|||
errors: IRSErrorDescription[]
|
||||
astText: string
|
||||
ast: SyntaxTree
|
||||
args: IFunctionArg[]
|
||||
}
|
||||
|
||||
export interface IRSExpression {
|
||||
|
@ -118,6 +124,7 @@ export interface IConstituenta {
|
|||
valueClass: ValueClass
|
||||
typification: string
|
||||
syntaxTree: string
|
||||
args: IFunctionArg[]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,6 +238,25 @@ export enum ExpressionStatus {
|
|||
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 =================
|
||||
export function inferStatus(parse?: ParsingStatus, value?: ValueClass): ExpressionStatus {
|
||||
if (!parse || !value) {
|
||||
|
@ -322,22 +348,23 @@ export function LoadRSFormData(schema: IRSFormData): IRSForm {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function matchConstituenta(query: string, target?: IConstituenta) {
|
||||
if (!target) {
|
||||
return false;
|
||||
} else if (target.alias.match(query)) {
|
||||
export function matchConstituenta(query: string, target: IConstituenta, mode: CstMatchMode) {
|
||||
if ((mode === CstMatchMode.ALL || mode === CstMatchMode.NAME) &&
|
||||
target.alias.match(query)) {
|
||||
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) {
|
||||
|
@ -350,3 +377,21 @@ export function matchRSFormMeta(query: string, target: IRSFormMeta) {
|
|||
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 { 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 {
|
||||
text: string
|
||||
|
@ -14,16 +17,6 @@ export interface IStatusInfo {
|
|||
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 {
|
||||
if (cst.cstType === CstType.STRUCTURED) {
|
||||
return (
|
||||
|
@ -273,6 +266,27 @@ export const mapLayoutLabels: Map<string, string> = new Map([
|
|||
['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}[] = [
|
||||
{ value: 'forceatlas2', label: 'Атлас 2D'},
|
||||
{ value: 'forceDirected2d', label: 'Силы 2D'},
|
||||
|
@ -360,7 +374,8 @@ export function getMockConstituenta(id: number, alias: string, type: CstType, co
|
|||
status: ParsingStatus.INCORRECT,
|
||||
valueClass: ValueClass.INVALID,
|
||||
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 {
|
||||
const id = error.errorType.toString(16)
|
||||
switch(resolveErrorClass(error.errorType)) {
|
||||
|
|
|
@ -7,3 +7,11 @@ export function assertIsNode(e: EventTarget | null): asserts e is Node {
|
|||
export async function delay(ms: number) {
|
||||
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 { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
Loading…
Reference in New Issue
Block a user