Compare commits

...

8 Commits

Author SHA1 Message Date
Ivan
01c0eb201e F: Implement dev helper features
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-07-28 13:07:00 +03:00
Ivan
81d378d076 B: Fix default tab bug 2024-07-28 11:38:14 +03:00
Ivan
c7da60325c Improve icons 2024-07-28 01:30:00 +03:00
Ivan
f67e304a79 Update Icons.tsx 2024-07-28 00:54:02 +03:00
Ivan
54ca6a5279 F: Implement RSForm to Operation dependency 2024-07-28 00:37:33 +03:00
Ivan
4899860a05 R: Move OSS to global library context 2024-07-27 22:50:10 +03:00
Ivan
342a1837ed M: Minor fixes 2024-07-27 22:49:20 +03:00
Ivan
12963c08dd F: Improve OSS UI controls 2024-07-26 21:08:31 +03:00
41 changed files with 1815 additions and 207 deletions

View File

@ -142,6 +142,7 @@
"setexpr", "setexpr",
"SIDELIST", "SIDELIST",
"signup", "signup",
"simplebezier",
"Slng", "Slng",
"SMALLPR", "SMALLPR",
"Stylesheet", "Stylesheet",

View File

@ -39,6 +39,7 @@ This readme file is used mostly to document project dependencies and conventions
- react-error-boundary - react-error-boundary
- react-pdf - react-pdf
- react-tooltip - react-tooltip
- react-zoom-pan-pinch
- reactflow - reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
@ -143,7 +144,8 @@ This readme file is used mostly to document project dependencies and conventions
- 🚀 F: major feature implementation - 🚀 F: major feature implementation
- 💄 D: UI design - 💄 D: UI design
- 🚑 B: bug fix - 🔥 B: bug fix
- 🚑 M: Minor fixes
- 🔧 R: refactoring and code improvement - 🔧 R: refactoring and code improvement
- 📝 I: documentation - 📝 I: documentation

View File

@ -4,8 +4,8 @@ from .basics import OperationPositionSerializer, PositionsSerializer, Substituti
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer OperationSerializer,
OperationTargetSerializer
) )
from .responses import NewOperationResponse from .responses import NewOperationResponse, NewSchemaResponse

View File

@ -53,7 +53,7 @@ class OperationCreateSerializer(serializers.Serializer):
) )
class OperationDeleteSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())
positions = serializers.ListField( positions = serializers.ListField(

View File

@ -1,6 +1,8 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer
from .data_access import OperationSchemaSerializer, OperationSerializer from .data_access import OperationSchemaSerializer, OperationSerializer
@ -8,3 +10,9 @@ class NewOperationResponse(serializers.Serializer):
''' Serializer: Create operation response. ''' ''' Serializer: Create operation response. '''
new_operation = OperationSerializer() new_operation = OperationSerializer()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class NewSchemaResponse(serializers.Serializer):
''' Serializer: Create RSForm for input operation response. '''
new_schema = LibraryItemSerializer()
oss = OperationSchemaSerializer()

View File

@ -127,8 +127,6 @@ class TestOssViewset(EndpointTester):
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self): def test_create_operation(self):
self.populateData() self.populateData()
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
@ -231,23 +229,6 @@ class TestOssViewset(EndpointTester):
self.assertEqual(schema.access_policy, self.owned.model.access_policy) self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location) self.assertEqual(schema.location, self.owned.model.location)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self): def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id) self.executeNotFound(item=self.invalid_id)
@ -269,3 +250,40 @@ class TestOssViewset(EndpointTester):
self.login() self.login()
response = self.executeOK(data=data) response = self.executeOK(data=data)
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['items']), 2)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.comment = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.sync_text = False
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(self.operation1.sync_text, True)
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['comment'], self.operation1.comment)
data['target'] = self.operation3.pk
self.executeBadData(data=data)

View File

@ -3,7 +3,7 @@ from typing import cast
from django.db import transaction from django.db import transaction
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics from rest_framework import generics, serializers
from rest_framework import status as c from rest_framework import status as c
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
@ -12,6 +12,7 @@ from rest_framework.response import Response
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from shared import messages as msg
from shared import permissions from shared import permissions
from .. import models as m from .. import models as m
@ -33,7 +34,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if self.action in [ if self.action in [
'create_operation', 'create_operation',
'delete_operation', 'delete_operation',
'update_positions' 'update_positions',
'create_input'
]: ]:
permission_list = [permissions.ItemEditor] permission_list = [permissions.ItemEditor]
elif self.action in ['details']: elif self.action in ['details']:
@ -117,19 +119,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.add_argument(operation=new_operation, argument=argument) oss.add_argument(operation=new_operation, argument=argument)
oss.refresh_from_db() oss.refresh_from_db()
response = Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': s.OperationSerializer(new_operation).data, 'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )
return response
@extend_schema( @extend_schema(
summary='delete operation', summary='delete operation',
tags=['OSS'], tags=['OSS'],
request=s.OperationDeleteSerializer, request=s.OperationTargetSerializer,
responses={ responses={
c.HTTP_200_OK: s.OperationSchemaSerializer, c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
@ -140,7 +141,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation') @action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk): def delete_operation(self, request: Request, pk):
''' Endpoint: Delete operation. ''' ''' Endpoint: Delete operation. '''
serializer = s.OperationDeleteSerializer( serializer = s.OperationTargetSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': self.get_object()}
) )
@ -156,3 +157,59 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data
) )
@extend_schema(
summary='create input schema for target operation',
tags=['OSS'],
request=s.OperationTargetSerializer(),
responses={
c.HTTP_200_OK: s.NewSchemaResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='create-input')
def create_input(self, request: Request, pk):
''' Create new input RSForm. '''
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.INPUT:
raise serializers.ValidationError({
'target': msg.operationNotInput(operation.alias)
})
if operation.result is not None:
raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias)
})
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
operation.result = schema
operation.sync_text = True
operation.save()
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
'new_schema': LibraryItemSerializer(schema).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)

View File

@ -18,6 +18,14 @@ def schemaNotOwned():
return 'Нет доступа к схеме' return 'Нет доступа к схеме'
def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}'
def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}'
def renameTrivial(name: str): def renameTrivial(name: str):
return f'Имя должно отличаться от текущего: {name}' return f'Имя должно отличаться от текущего: {name}'

View File

@ -29,6 +29,7 @@
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^10.0.5", "react-toastify": "^10.0.5",
"react-tooltip": "^5.27.1", "react-tooltip": "^5.27.1",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"reagraph": "^4.19.2", "reagraph": "^4.19.2",
"use-debounce": "^10.0.1" "use-debounce": "^10.0.1"
@ -10542,6 +10543,20 @@
"react-dom": ">=16.13" "react-dom": ">=16.13"
} }
}, },
"node_modules/react-zoom-pan-pinch": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.6.1.tgz",
"integrity": "sha512-SdPqdk7QDSV7u/WulkFOi+cnza8rEZ0XX4ZpeH7vx3UZEg7DoyuAy3MCmm+BWv/idPQL2Oe73VoC0EhfCN+sZQ==",
"license": "MIT",
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/reactflow": { "node_modules/reactflow": {
"version": "11.11.4", "version": "11.11.4",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",

View File

@ -33,6 +33,7 @@
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^10.0.5", "react-toastify": "^10.0.5",
"react-tooltip": "^5.27.1", "react-tooltip": "^5.27.1",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"reagraph": "^4.19.2", "reagraph": "^4.19.2",
"use-debounce": "^10.0.1" "use-debounce": "^10.0.1"

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -3,10 +3,13 @@ import {
IconAdminOff, IconAdminOff,
IconDarkTheme, IconDarkTheme,
IconDatabase, IconDatabase,
IconDBStructure,
IconHelp, IconHelp,
IconHelpOff, IconHelpOff,
IconImage,
IconLightTheme, IconLightTheme,
IconLogout, IconLogout,
IconRESTapi,
IconUser IconUser
} from '@/components/Icons'; } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
@ -43,6 +46,21 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
logout(() => router.push(urls.admin, true)); logout(() => router.push(urls.admin, true));
} }
function gotoIcons(event: CProps.EventMouse) {
hideDropdown();
router.push(urls.icons, event.ctrlKey || event.metaKey);
}
function gotoRestApi() {
hideDropdown();
router.push(urls.rest_api, true);
}
function gotoDatabaseSchema(event: CProps.EventMouse) {
hideDropdown();
router.push(urls.database_schema, event.ctrlKey || event.metaKey);
}
function handleToggleDarkMode() { function handleToggleDarkMode() {
hideDropdown(); hideDropdown();
toggleDarkMode(); toggleDarkMode();
@ -77,7 +95,34 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
/> />
) : null} ) : null}
{user?.is_staff ? ( {user?.is_staff ? (
<DropdownButton text='База данных' icon={<IconDatabase size='1rem' />} onClick={gotoAdmin} /> <DropdownButton
text='REST API' // prettier: split-line
icon={<IconRESTapi size='1rem' />}
className='border-t'
onClick={gotoRestApi}
/>
) : null}
{user?.is_staff ? (
<DropdownButton
text='База данных' // prettier: split-line
icon={<IconDatabase size='1rem' />}
onClick={gotoAdmin}
/>
) : null}
{user?.is_staff ? (
<DropdownButton
text='Иконки' // prettier: split-line
icon={<IconImage size='1rem' />}
onClick={gotoIcons}
/>
) : null}
{user?.is_staff ? (
<DropdownButton
text='Структура БД' // prettier: split-line
icon={<IconDBStructure size='1rem' />}
onClick={gotoDatabaseSchema}
className='border-b'
/>
) : null} ) : null}
<DropdownButton <DropdownButton
text='Выйти...' text='Выйти...'

View File

@ -1,6 +1,7 @@
import { createBrowserRouter } from 'react-router-dom'; import { createBrowserRouter } from 'react-router-dom';
import CreateItemPage from '@/pages/CreateItemPage'; import CreateItemPage from '@/pages/CreateItemPage';
import DatabaseSchemaPage from '@/pages/DatabaseSchemaPage';
import HomePage from '@/pages/HomePage'; import HomePage from '@/pages/HomePage';
import IconsPage from '@/pages/IconsPage'; import IconsPage from '@/pages/IconsPage';
import LibraryPage from '@/pages/LibraryPage'; import LibraryPage from '@/pages/LibraryPage';
@ -63,13 +64,17 @@ export const Router = createBrowserRouter([
path: `${routes.oss}/:id`, path: `${routes.oss}/:id`,
element: <OssPage /> element: <OssPage />
}, },
{
path: routes.manuals,
element: <ManualsPage />
},
{ {
path: `${routes.icons}`, path: `${routes.icons}`,
element: <IconsPage /> element: <IconsPage />
}, },
{ {
path: routes.manuals, path: `${routes.database_schema}`,
element: <ManualsPage /> element: <DatabaseSchemaPage />
} }
] ]
} }

View File

@ -19,7 +19,8 @@ export const routes = {
help: 'manuals', help: 'manuals',
rsforms: 'rsforms', rsforms: 'rsforms',
oss: 'oss', oss: 'oss',
icons: 'icons' icons: 'icons',
database_schema: 'database-schema'
}; };
interface SchemaProps { interface SchemaProps {
@ -39,10 +40,13 @@ interface OssProps {
*/ */
export const urls = { export const urls = {
admin: `${buildConstants.backend}/admin`, admin: `${buildConstants.backend}/admin`,
rest_api: `${buildConstants.backend}/`,
home: '/', home: '/',
login: `/${routes.login}`, login: `/${routes.login}`,
login_hint: (userName: string) => `/login?username=${userName}`, login_hint: (userName: string) => `/login?username=${userName}`,
profile: `/${routes.profile}`, profile: `/${routes.profile}`,
icons: `/${routes.icons}`,
database_schema: `/${routes.database_schema}`,
signup: `/${routes.signup}`, signup: `/${routes.signup}`,
library: `/${routes.library}`, library: `/${routes.library}`,
library_filter: (strategy: string) => `/library?filter=${strategy}`, library_filter: (strategy: string) => `/library?filter=${strategy}`,
@ -51,7 +55,7 @@ export const urls = {
help_topic: (topic: string) => `/manuals?topic=${topic}`, help_topic: (topic: string) => `/manuals?topic=${topic}`,
schema: (id: number | string, version?: number | string) => schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''), `/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
oss: (id: number | string) => `/oss/${id}`, oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
schema_props: ({ id, tab, version, active }: SchemaProps) => { schema_props: ({ id, tab, version, active }: SchemaProps) => {
const versionStr = version !== undefined ? `v=${version}&` : ''; const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : ''; const activeStr = active !== undefined ? `&active=${active}` : '';

View File

@ -3,6 +3,7 @@
*/ */
import { import {
IInputCreatedResponse,
IOperationCreateData, IOperationCreateData,
IOperationCreatedResponse, IOperationCreatedResponse,
IOperationSchemaData, IOperationSchemaData,
@ -19,26 +20,33 @@ export function getOssDetails(target: string, request: FrontPull<IOperationSchem
}); });
} }
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) { export function patchUpdatePositions(oss: string, request: FrontPush<IPositionsData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/oss/${schema}/update-positions`, endpoint: `/api/oss/${oss}/update-positions`,
request: request request: request
}); });
} }
export function postCreateOperation( export function postCreateOperation(
schema: string, oss: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse> request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) { ) {
AxiosPost({ AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`, endpoint: `/api/oss/${oss}/create-operation`,
request: request request: request
}); });
} }
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) { export function patchDeleteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/oss/${schema}/delete-operation`, endpoint: `/api/oss/${oss}/delete-operation`,
request: request
});
}
export function patchCreateInput(oss: string, request: FrontExchange<ITargetOperation, IInputCreatedResponse>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/create-input`,
request: request request: request
}); });
} }

View File

@ -38,7 +38,6 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu'; export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { LuLightbulb as IconHelp } from 'react-icons/lu'; export { LuLightbulb as IconHelp } from 'react-icons/lu';
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu'; export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { TbGridDots as IconGrid } from 'react-icons/tb';
export { RiPushpinFill as IconPin } from 'react-icons/ri'; export { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
export { BiCaretDown as IconSortDesc } from 'react-icons/bi'; export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
@ -62,10 +61,12 @@ export { TbBriefcase as IconBusiness } from 'react-icons/tb';
export { VscLibrary as IconLibrary } from 'react-icons/vsc'; export { VscLibrary as IconLibrary } from 'react-icons/vsc';
export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { FaRegObjectGroup as IconOSS } from 'react-icons/fa'; export { GiHoneycomb as IconOSS } from 'react-icons/gi';
export { RiHexagonLine as IconRSForm } from 'react-icons/ri'; export { RiHexagonLine as IconRSForm } from 'react-icons/ri';
export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuArchive as IconArchive } from 'react-icons/lu';
export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu';
export { LuView as IconDBStructure } from 'react-icons/lu';
export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu';
export { TbColumns as IconList } from 'react-icons/tb'; export { TbColumns as IconList } from 'react-icons/tb';
export { ImStack as IconVersions } from 'react-icons/im'; export { ImStack as IconVersions } from 'react-icons/im';
@ -106,6 +107,8 @@ export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWand2 as IconGenerateNames } from 'react-icons/lu'; export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr';
export { BsPlay as IconExecute } from 'react-icons/bs';
// ======== Graph UI ======= // ======== Graph UI =======
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi'; export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
@ -118,6 +121,11 @@ export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
export { LuSparkles as IconClustering } from 'react-icons/lu'; export { LuSparkles as IconClustering } from 'react-icons/lu';
export { LuSparkle as IconClusteringOff } from 'react-icons/lu'; export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
export { TbGridDots as IconGrid } from 'react-icons/tb';
export { FaSlash as IconLineStraight } from 'react-icons/fa6';
export { PiWaveSineLight as IconLineWave } from 'react-icons/pi';
export { LuCircleDashed as IconAnimation } from 'react-icons/lu';
export { LuCircle as IconAnimationOff } from 'react-icons/lu';
// ===== Custom elements ====== // ===== Custom elements ======
interface IconSVGProps { interface IconSVGProps {

View File

@ -0,0 +1,103 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { IconRemove } from '@/components/Icons';
import SelectOperation from '@/components/select/SelectOperation';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton';
import NoData from '@/components/ui/NoData';
import { IOperation, OperationID } from '@/models/oss';
interface PickMultiOperationProps {
rows?: number;
items: IOperation[];
selected: OperationID[];
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
}
const columnHelper = createColumnHelper<IOperation>();
function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOperationProps) {
const selectedItems = useMemo(() => items.filter(item => selected.includes(item.id)), [items, selected]);
const nonSelectedItems = useMemo(() => items.filter(item => !selected.includes(item.id)), [items, selected]);
const [lastSelected, setLastSelected] = useState<IOperation | undefined>(undefined);
const handleDelete = useCallback(
(operation: OperationID) => setSelected(prev => prev.filter(item => item !== operation)),
[setSelected]
);
const handleSelect = useCallback(
(operation?: IOperation) => {
if (operation) {
setLastSelected(operation);
setSelected(prev => [...prev, operation.id]);
setTimeout(() => setLastSelected(undefined), 1000);
}
},
[setSelected]
);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Шифр',
size: 150,
minSize: 80,
maxSize: 150
}),
columnHelper.accessor('title', {
id: 'title',
header: 'Название',
size: 1200,
minSize: 200,
maxSize: 1200,
cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
}),
columnHelper.display({
id: 'actions',
cell: props => (
<MiniButton
noHover
title='Удалить'
icon={<IconRemove size='1rem' className='icon-red' />}
onClick={() => handleDelete(props.row.original.id)}
/>
)
})
],
[handleDelete]
);
return (
<div className='flex flex-col gap-1 border-t border-x rounded-t-md clr-input'>
<SelectOperation
noBorder
items={nonSelectedItems} // prettier: split-line
value={lastSelected}
onSelectValue={handleSelect}
className='w-full'
/>
<DataTable
dense
noFooter
rows={rows}
contentHeight='1.3rem'
className='cc-scroll-y text-sm select-none border-y'
data={selectedItems}
columns={columns}
headPosition='0rem'
noDataComponent={
<NoData>
<p>Список пуст</p>
</NoData>
}
/>
</div>
);
}
export default PickMultiOperation;

View File

@ -15,7 +15,9 @@ interface SelectConstituentaProps extends CProps.Styling {
items?: IConstituenta[]; items?: IConstituenta[];
value?: IConstituenta; value?: IConstituenta;
onSelectValue: (newValue?: IConstituenta) => void; onSelectValue: (newValue?: IConstituenta) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean;
} }
function SelectConstituenta({ function SelectConstituenta({

View File

@ -13,7 +13,9 @@ interface SelectOperationProps extends CProps.Styling {
items?: IOperation[]; items?: IOperation[];
value?: IOperation; value?: IOperation;
onSelectValue: (newValue?: IOperation) => void; onSelectValue: (newValue?: IOperation) => void;
placeholder?: string; placeholder?: string;
noBorder?: boolean;
} }
function SelectOperation({ function SelectOperation({

View File

@ -13,8 +13,10 @@ import SelectSingle from '../ui/SelectSingle';
interface SelectUserProps extends CProps.Styling { interface SelectUserProps extends CProps.Styling {
items?: IUserInfo[]; items?: IUserInfo[];
value?: UserID; value?: UserID;
placeholder?: string;
onSelectValue: (newValue: UserID) => void; onSelectValue: (newValue: UserID) => void;
placeholder?: string;
noBorder?: boolean;
} }
function SelectUser({ function SelectUser({

View File

@ -14,6 +14,9 @@ interface SelectVersionProps extends CProps.Styling {
items?: IVersionInfo[]; items?: IVersionInfo[];
value?: VersionID; value?: VersionID;
onSelectValue: (newValue?: VersionID) => void; onSelectValue: (newValue?: VersionID) => void;
placeholder?: string;
noBorder?: boolean;
} }
function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) { function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) {

View File

@ -13,11 +13,13 @@ import {
} from '@/backend/library'; } from '@/backend/library';
import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms'; import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { FolderTree } from '@/models/FolderTree'; import { FolderTree } from '@/models/FolderTree';
import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library'; import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'; import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader'; import { RSFormLoader } from '@/models/RSFormLoader';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -34,10 +36,17 @@ interface ILibraryContext {
loadingError: ErrorData; loadingError: ErrorData;
setLoadingError: (error: ErrorData) => void; setLoadingError: (error: ErrorData) => void;
globalOSS: IOperationSchema | undefined;
setGlobalID: (id: string | undefined) => void;
setGlobalOSS: (data: IOperationSchemaData) => void;
ossLoading: boolean;
ossError: ErrorData;
processing: boolean; processing: boolean;
processingError: ErrorData; processingError: ErrorData;
setProcessingError: (error: ErrorData) => void; setProcessingError: (error: ErrorData) => void;
reloadOSS: (callback?: () => void) => void;
reloadItems: (callback?: () => void) => void; reloadItems: (callback?: () => void) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[]; applyFilter: (params: ILibraryFilter) => ILibraryItem[];
@ -75,6 +84,22 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]); const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
const [ossID, setGlobalID] = useState<string | undefined>(undefined);
const {
schema: globalOSS, // prettier: split lines
error: ossError,
setSchema: setGlobalOSS,
loading: ossLoading,
reload: reloadOssInternal
} = useOssDetails({ target: ossID });
const reloadOSS = useCallback(
(callback?: () => void) => {
reloadOssInternal(setProcessing, callback);
},
[reloadOssInternal]
);
const folders = useMemo(() => { const folders = useMemo(() => {
const result = new FolderTree(); const result = new FolderTree();
result.addPath(LocationHead.USER, 0); result.addPath(LocationHead.USER, 0);
@ -255,11 +280,17 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
1 1
); );
} }
if (globalOSS?.schemas.includes(target)) {
reloadOSS(() => {
if (callback) callback(); if (callback) callback();
});
} else {
if (callback) callback();
}
}) })
}); });
}, },
[reloadItems, user] [reloadItems, reloadOSS, user, globalOSS]
); );
const cloneItem = useCallback( const cloneItem = useCallback(
@ -300,6 +331,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
processingError, processingError,
setProcessingError, setProcessingError,
globalOSS,
setGlobalID,
setGlobalOSS,
ossLoading,
ossError,
reloadOSS,
reloadItems, reloadItems,
applyFilter, applyFilter,

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { import {
@ -12,12 +12,18 @@ import {
patchSetOwner, patchSetOwner,
postSubscribe postSubscribe
} from '@/backend/library'; } from '@/backend/library';
import { patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss'; import { patchCreateInput, patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
import { IOperation, IOperationCreateData, IOperationSchema, IPositionsData, ITargetOperation } from '@/models/oss'; import {
IOperation,
IOperationCreateData,
IOperationSchema,
IOperationSchemaData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -48,6 +54,7 @@ interface IOssContext {
savePositions: (data: IPositionsData, callback?: () => void) => void; savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
} }
const OssContext = createContext<IOssContext | null>(null); const OssContext = createContext<IOssContext | null>(null);
@ -66,13 +73,8 @@ interface OssStateProps {
export const OssState = ({ itemID, children }: OssStateProps) => { export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary(); const library = useLibrary();
const schema = library.globalOSS;
const { user } = useAuth(); const { user } = useAuth();
const {
schema, // prettier: split lines
error: errorLoading,
setSchema,
loading
} = useOssDetails({ target: itemID });
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
@ -90,6 +92,12 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, schema, toggleTracking]); }, [user, schema, toggleTracking]);
useEffect(() => {
if (schema?.id !== Number(itemID)) {
library.setGlobalID(itemID);
}
}, [itemID, schema, library]);
const update = useCallback( const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => { (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) { if (!schema) {
@ -102,13 +110,14 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(Object.assign(schema, newData)); const fullData: IOperationSchemaData = Object.assign(schema, newData);
library.setGlobalOSS(fullData);
library.localUpdateItem(newData); library.localUpdateItem(newData);
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, },
[itemID, setSchema, schema, library] [itemID, schema, library]
); );
const subscribe = useCallback( const subscribe = useCallback(
@ -133,7 +142,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
} }
}); });
}, },
[itemID, schema, user] [itemID, user, schema]
); );
const unsubscribe = useCallback( const unsubscribe = useCallback(
@ -278,13 +287,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData.oss); library.setGlobalOSS(newData.oss);
library.localUpdateTimestamp(newData.oss.id); library.localUpdateTimestamp(newData.oss.id);
if (callback) callback(newData.new_operation); if (callback) callback(newData.new_operation);
} }
}); });
}, },
[itemID, library, setSchema] [itemID, library]
); );
const deleteOperation = useCallback( const deleteOperation = useCallback(
@ -296,13 +305,32 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: setProcessingError, onError: setProcessingError,
onSuccess: newData => { onSuccess: newData => {
setSchema(newData); library.setGlobalOSS(newData);
library.localUpdateTimestamp(newData.id); library.localUpdateTimestamp(newData.id);
if (callback) callback(); if (callback) callback();
} }
}); });
}, },
[itemID, library, setSchema] [itemID, library]
);
const createInput = useCallback(
(data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => {
setProcessingError(undefined);
patchCreateInput(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData.oss);
library.reloadItems(() => {
if (callback) callback(newData.new_schema);
});
}
});
},
[itemID, library]
); );
return ( return (
@ -310,8 +338,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
value={{ value={{
schema, schema,
itemID, itemID,
loading, loading: library.ossLoading,
errorLoading, errorLoading: library.ossError,
processing, processing,
processingError, processingError,
isOwned, isOwned,
@ -327,7 +355,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
savePositions, savePositions,
createOperation, createOperation,
deleteOperation deleteOperation,
createInput
}} }}
> >
{children} {children}

View File

@ -1,16 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
import SelectOperation from '@/components/select/SelectOperation';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperation, IOperationSchema, OperationID } from '@/models/oss'; import { IOperationSchema, OperationID } from '@/models/oss';
import { limits, patterns } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
import PickMultiOperation from '../../components/select/PickMultiOperation';
interface TabSynthesisOperationProps { interface TabSynthesisOperationProps {
oss: IOperationSchema; oss: IOperationSchema;
alias: string; alias: string;
@ -34,22 +31,6 @@ function TabSynthesisOperation({
inputs, inputs,
setInputs setInputs
}: TabSynthesisOperationProps) { }: TabSynthesisOperationProps) {
const [left, setLeft] = useState<IOperation | undefined>(undefined);
const [right, setRight] = useState<IOperation | undefined>(undefined);
console.log(inputs);
useEffect(() => {
const inputs: OperationID[] = [];
if (left) {
inputs.push(left.id);
}
if (right) {
inputs.push(right.id);
}
setInputs(inputs);
}, [setInputs, left, right]);
return ( return (
<AnimateFade className='cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
@ -79,16 +60,10 @@ function TabSynthesisOperation({
/> />
</div> </div>
<div className='flex justify-between'>
<FlexColumn> <FlexColumn>
<Label text='Аргумент 1' /> <Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<SelectOperation items={oss.items} value={left} onSelectValue={setLeft} /> <PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} />
</FlexColumn> </FlexColumn>
<FlexColumn>
<Label text='Аргумент 2' className='text-right' />
<SelectOperation items={oss.items} value={right} onSelectValue={setRight} />
</FlexColumn>
</div>
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -1,4 +1,3 @@
@import 'styling/setup.css'; @import 'styling/setup.css';
@import 'styling/styles.css';
@import 'styling/imports.css';
@import 'styling/overrides.css'; @import 'styling/overrides.css';
@import 'styling/styles.css';

View File

@ -40,6 +40,7 @@ export interface OssNodeInternal {
label: string; label: string;
operation: IOperation; operation: IOperation;
}; };
dragging: boolean;
xPos: number; xPos: number;
yPos: number; yPos: number;
} }

View File

@ -3,7 +3,7 @@
*/ */
import { Graph } from './Graph'; import { Graph } from './Graph';
import { ILibraryItemData, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library';
import { ConstituentaID } from './rsform'; import { ConstituentaID } from './rsform';
/** /**
@ -139,3 +139,11 @@ export interface IOperationCreatedResponse {
new_operation: IOperation; new_operation: IOperation;
oss: IOperationSchemaData; oss: IOperationSchemaData;
} }
/**
* Represents data response when creating {@link IRSForm} for Input {@link IOperation}.
*/
export interface IInputCreatedResponse {
new_schema: ILibraryItem;
oss: IOperationSchemaData;
}

View File

@ -0,0 +1,31 @@
'use client';
import { useLayoutEffect, useMemo } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { resources } from '@/utils/constants';
function DatabaseSchemaPage() {
const { calculateHeight, setNoFooter } = useConceptOptions();
const panelHeight = useMemo(() => calculateHeight('0px'), [calculateHeight]);
useLayoutEffect(() => {
setNoFooter(true);
return () => setNoFooter(false);
}, [setNoFooter]);
return (
<AnimateFade className='flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>
<TransformWrapper>
<TransformComponent>
<img alt='Схема базы данных' src={resources.db_schema} className='w-fit h-fit' />
</TransformComponent>
</TransformWrapper>
</AnimateFade>
);
}
export default DatabaseSchemaPage;

View File

@ -4,13 +4,12 @@
import * as icons from '@/components/Icons'; import * as icons from '@/components/Icons';
export function IconsPage() { export function IconsPage() {
const iconsList = Object.keys(icons).filter(key => key.startsWith('Icon'));
return ( return (
<div className='flex flex-col items-center px-6 py-3'> <div className='flex flex-col items-center px-6 py-3'>
<h1 className='mb-6'>Список иконок</h1> <h1 className='mb-6'>Всего иконок: {iconsList.length}</h1>
<div className='grid grid-cols-4'> <div className='grid grid-cols-4'>
{Object.keys(icons) {iconsList.map((key, index) => (
.filter(key => key.startsWith('Icon'))
.map((key, index) => (
<div key={`icons_list_${index}`} className='flex flex-col items-center px-3 pb-6'> <div key={`icons_list_${index}`} className='flex flex-col items-center px-3 pb-6'>
<p>{icons[key]({ size: '2rem' })}</p> <p>{icons[key]({ size: '2rem' })}</p>
<p>{key}</p> <p>{key}</p>

View File

@ -2,9 +2,6 @@
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import useLocalStorage from '@/hooks/useLocalStorage';
import { storage } from '@/utils/constants';
import OssFlow from './OssFlow'; import OssFlow from './OssFlow';
interface EditorOssGraphProps { interface EditorOssGraphProps {
@ -13,11 +10,9 @@ interface EditorOssGraphProps {
} }
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) { function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
const [showGrid, setShowGrid] = useLocalStorage<boolean>(storage.ossShowGrid, false);
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<OssFlow isModified={isModified} setIsModified={setIsModified} showGrid={showGrid} setShowGrid={setShowGrid} /> <OssFlow isModified={isModified} setIsModified={setIsModified} />
</ReactFlowProvider> </ReactFlowProvider>
); );
} }

View File

@ -26,6 +26,7 @@ function InputNode(node: OssNodeInternal) {
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover
title='Связанная КС' title='Связанная КС'
hideTitle={!controller.showTooltip}
onClick={() => { onClick={() => {
handleOpenSchema(); handleOpenSchema();
}} }}
@ -34,7 +35,7 @@ function InputNode(node: OssNodeInternal) {
</Overlay> </Overlay>
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
{node.data.label} {node.data.label}
{controller.showTooltip ? ( {controller.showTooltip && !node.dragging ? (
<TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} /> <TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
) : null} ) : null}
</div> </div>

View File

@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconDestroy, IconEdit2, IconNewItem, IconRSForm } from '@/components/Icons'; import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import useClickedOutside from '@/hooks/useClickedOutside'; import useClickedOutside from '@/hooks/useClickedOutside';
@ -22,9 +22,10 @@ export interface ContextMenuData {
interface NodeContextMenuProps extends ContextMenuData { interface NodeContextMenuProps extends ContextMenuData {
onHide: () => void; onHide: () => void;
onDelete: (target: OperationID) => void; onDelete: (target: OperationID) => void;
onCreateInput: (target: OperationID) => void;
} }
function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: NodeContextMenuProps) { function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCreateInput }: NodeContextMenuProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null); const ref = useRef(null);
@ -57,11 +58,21 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: Node
onDelete(operation.id); onDelete(operation.id);
}; };
const handleCreateSchema = () => {
handleHide();
onCreateInput(operation.id);
};
const handleRunSynthesis = () => {
toast.error('Not implemented');
handleHide();
};
return ( return (
<div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}> <div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}>
<Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}> <Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}>
<DropdownButton <DropdownButton
text='Свойства операции' text='Редактировать'
titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')} titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')}
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
@ -83,16 +94,25 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: Node
title='Создать пустую схему для загрузки' title='Создать пустую схему для загрузки'
icon={<IconNewItem size='1rem' className='icon-green' />} icon={<IconNewItem size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleCreateSchema}
/>
) : null}
{controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? (
<DropdownButton
text='Загрузить схему'
title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
onClick={handleEditSchema} onClick={handleEditSchema}
/> />
) : null} ) : null}
{controller.isMutable && operation.operation_type === OperationType.INPUT ? ( {controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton <DropdownButton
text='Привязать схему' text='Выполнить синтез'
title='Выбрать схему для загрузки' title='Выполнить операцию и получить синтезированную КС'
icon={<IconRSForm size='1rem' className='icon-primary' />} icon={<IconExecute size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleEditSchema} onClick={handleRunSynthesis}
/> />
) : null} ) : null}

View File

@ -27,6 +27,7 @@ function OperationNode(node: OssNodeInternal) {
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover
title='Связанная КС' title='Связанная КС'
hideTitle={!controller.showTooltip}
onClick={() => { onClick={() => {
handleOpenSchema(); handleOpenSchema();
}} }}
@ -36,7 +37,9 @@ function OperationNode(node: OssNodeInternal) {
<div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'> <div id={`${prefixes.operation_list}${node.id}`} className='flex-grow text-center text-sm'>
{node.data.label} {node.data.label}
{controller.showTooltip && !node.dragging ? (
<TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} /> <TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
) : null}
</div> </div>
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} /> <Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />

View File

@ -10,7 +10,6 @@ import {
Node, Node,
NodeChange, NodeChange,
NodeTypes, NodeTypes,
ProOptions,
ReactFlow, ReactFlow,
useEdgesState, useEdgesState,
useNodesState, useNodesState,
@ -23,9 +22,10 @@ import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import { OssNode } from '@/models/miscellaneous'; import { OssNode } from '@/models/miscellaneous';
import { OperationID } from '@/models/oss'; import { OperationID } from '@/models/oss';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER, storage } from '@/utils/constants';
import { errors } from '@/utils/labels'; import { errors } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
@ -37,16 +37,18 @@ import ToolbarOssGraph from './ToolbarOssGraph';
interface OssFlowProps { interface OssFlowProps {
isModified: boolean; isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>; setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
showGrid: boolean;
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
} }
function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) { function OssFlow({ isModified, setIsModified }: OssFlowProps) {
const { calculateHeight, colors } = useConceptOptions(); const { calculateHeight, colors } = useConceptOptions();
const model = useOSS(); const model = useOSS();
const controller = useOssEdit(); const controller = useOssEdit();
const flow = useReactFlow(); const flow = useReactFlow();
const [showGrid, setShowGrid] = useLocalStorage<boolean>(storage.ossShowGrid, false);
const [edgeAnimate, setEdgeAnimate] = useLocalStorage<boolean>(storage.ossEdgeAnimate, false);
const [edgeStraight, setEdgeStraight] = useLocalStorage<boolean>(storage.ossEdgeStraight, false);
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
@ -81,6 +83,8 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
id: String(index), id: String(index),
source: String(argument.argument), source: String(argument.argument),
target: String(argument.operation), target: String(argument.operation),
type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate,
targetHandle: targetHandle:
model.schema!.operationByID.get(argument.argument)!.position_x > model.schema!.operationByID.get(argument.argument)!.position_x >
model.schema!.operationByID.get(argument.operation)!.position_x model.schema!.operationByID.get(argument.operation)!.position_x
@ -92,7 +96,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
setTimeout(() => { setTimeout(() => {
setIsModified(false); setIsModified(false);
}, PARAMETER.graphRefreshDelay); }, PARAMETER.graphRefreshDelay);
}, [model.schema, setNodes, setEdges, setIsModified, toggleReset]); }, [model.schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]);
const getPositions = useCallback( const getPositions = useCallback(
() => () =>
@ -137,6 +141,13 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
[controller, getPositions] [controller, getPositions]
); );
const handleCreateInput = useCallback(
(target: OperationID) => {
controller.createInput(target, getPositions());
},
[controller, getPositions]
);
const handleFitView = useCallback(() => { const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration }); flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]); }, [flow]);
@ -180,7 +191,6 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: CProps.EventMouse, node: OssNode) => { (event: CProps.EventMouse, node: OssNode) => {
console.log(node);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -224,7 +234,6 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
} }
} }
const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []);
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []); const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
@ -244,7 +253,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
fitView fitView
proOptions={proOptions} proOptions={{ hideAttribution: true }}
nodeTypes={OssNodeTypes} nodeTypes={OssNodeTypes}
maxZoom={2} maxZoom={2}
minZoom={0.75} minZoom={0.75}
@ -257,17 +266,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
{showGrid ? <Background gap={10} /> : null} {showGrid ? <Background gap={10} /> : null}
</ReactFlow> </ReactFlow>
), ),
[ [nodes, edges, handleNodesChange, handleContextMenu, handleClickCanvas, onEdgesChange, OssNodeTypes, showGrid]
nodes,
edges,
proOptions,
handleNodesChange,
handleContextMenu,
handleClickCanvas,
onEdgesChange,
OssNodeTypes,
showGrid
]
); );
return ( return (
@ -276,6 +275,8 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
<ToolbarOssGraph <ToolbarOssGraph
isModified={isModified} isModified={isModified}
showGrid={showGrid} showGrid={showGrid}
edgeAnimate={edgeAnimate}
edgeStraight={edgeStraight}
onFitView={handleFitView} onFitView={handleFitView}
onCreate={handleCreateOperation} onCreate={handleCreateOperation}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
@ -283,10 +284,17 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
onSavePositions={handleSavePositions} onSavePositions={handleSavePositions}
onSaveImage={handleSaveImage} onSaveImage={handleSaveImage}
toggleShowGrid={() => setShowGrid(prev => !prev)} toggleShowGrid={() => setShowGrid(prev => !prev)}
toggleEdgeAnimate={() => setEdgeAnimate(prev => !prev)}
toggleEdgeStraight={() => setEdgeStraight(prev => !prev)}
/> />
</Overlay> </Overlay>
{menuProps ? ( {menuProps ? (
<NodeContextMenu onHide={handleContextMenuHide} onDelete={handleDeleteOperation} {...menuProps} /> <NodeContextMenu
onHide={handleContextMenuHide}
onDelete={handleDeleteOperation}
onCreateInput={handleCreateInput}
{...menuProps}
/>
) : null} ) : null}
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}> <div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
{graph} {graph}

View File

@ -1,6 +1,18 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconDestroy, IconFitImage, IconGrid, IconImage, IconNewItem, IconReset, IconSave } from '@/components/Icons'; import {
IconAnimation,
IconAnimationOff,
IconDestroy,
IconFitImage,
IconGrid,
IconImage,
IconLineStraight,
IconLineWave,
IconNewItem,
IconReset,
IconSave
} from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -12,6 +24,8 @@ import { useOssEdit } from '../OssEditContext';
interface ToolbarOssGraphProps { interface ToolbarOssGraphProps {
isModified: boolean; isModified: boolean;
showGrid: boolean; showGrid: boolean;
edgeAnimate: boolean;
edgeStraight: boolean;
onCreate: () => void; onCreate: () => void;
onDelete: () => void; onDelete: () => void;
onFitView: () => void; onFitView: () => void;
@ -19,39 +33,30 @@ interface ToolbarOssGraphProps {
onSavePositions: () => void; onSavePositions: () => void;
onResetPositions: () => void; onResetPositions: () => void;
toggleShowGrid: () => void; toggleShowGrid: () => void;
toggleEdgeAnimate: () => void;
toggleEdgeStraight: () => void;
} }
function ToolbarOssGraph({ function ToolbarOssGraph({
isModified, isModified,
showGrid, showGrid,
edgeAnimate,
edgeStraight,
onCreate, onCreate,
onDelete, onDelete,
onFitView, onFitView,
onSaveImage, onSaveImage,
onSavePositions, onSavePositions,
onResetPositions, onResetPositions,
toggleShowGrid toggleShowGrid,
toggleEdgeAnimate,
toggleEdgeStraight
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const controller = useOssEdit(); const controller = useOssEdit();
return ( return (
<div className='flex flex-col items-center'>
<div className='cc-icons'> <div className='cc-icons'>
{controller.isMutable ? (
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={controller.isProcessing || !isModified}
onClick={onSavePositions}
/>
) : null}
{controller.isMutable ? (
<MiniButton
title='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isModified}
onClick={onResetPositions}
/>
) : null}
<MiniButton <MiniButton
icon={<IconFitImage size='1.25rem' className='icon-primary' />} icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Сбросить вид' title='Сбросить вид'
@ -68,22 +73,28 @@ function ToolbarOssGraph({
} }
onClick={toggleShowGrid} onClick={toggleShowGrid}
/> />
{controller.isMutable ? (
<MiniButton <MiniButton
title='Новая операция' title={edgeStraight ? 'Связи: прямые' : 'Связи: безье'}
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={
disabled={controller.isProcessing} edgeStraight ? (
onClick={onCreate} <IconLineStraight size='1.25rem' className='icon-primary' />
) : (
<IconLineWave size='1.25rem' className='icon-primary' />
)
}
onClick={toggleEdgeStraight}
/> />
) : null}
{controller.isMutable ? (
<MiniButton <MiniButton
title='Удалить выбранную' title={edgeAnimate ? 'Анимация: вкл' : 'Анимация: выкл'}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={
disabled={controller.selected.length !== 1 || controller.isProcessing} edgeAnimate ? (
onClick={onDelete} <IconAnimation size='1.25rem' className='icon-primary' />
) : (
<IconAnimationOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleEdgeAnimate}
/> />
) : null}
<MiniButton <MiniButton
icon={<IconImage size='1.25rem' className='icon-primary' />} icon={<IconImage size='1.25rem' className='icon-primary' />}
title='Сохранить изображение' title='Сохранить изображение'
@ -95,6 +106,36 @@ function ToolbarOssGraph({
offset={4} offset={4}
/> />
</div> </div>
{controller.isMutable ? (
<div className='cc-icons'>
{' '}
<MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={controller.isProcessing || !isModified}
onClick={onSavePositions}
/>
<MiniButton
title='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isModified}
onClick={onResetPositions}
/>
<MiniButton
title='Новая операция'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={onCreate}
/>
<MiniButton
title='Удалить выбранную'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.selected.length !== 1 || controller.isProcessing}
onClick={onDelete}
/>
</div>
) : null}
</div>
); );
} }

View File

@ -44,6 +44,7 @@ export interface IOssEditContext {
savePositions: (positions: IOperationPosition[], callback?: () => void) => void; savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void; promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -210,6 +211,16 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model] [model]
); );
const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
model.createInput({ target: target, positions: positions }, new_schema => {
toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id));
});
},
[model, router]
);
return ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
@ -234,7 +245,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
openOperationSchema, openOperationSchema,
savePositions, savePositions,
promptCreateOperation, promptCreateOperation,
deleteOperation deleteOperation,
createInput
}} }}
> >
{model.schema ? ( {model.schema ? (

View File

@ -33,7 +33,7 @@ export enum OssTabID {
function OssTabs() { function OssTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = (Number(query.get('tab')) ?? OssTabID.CARD) as OssTabID; const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading } = useOSS(); const { schema, loading, errorLoading } = useOSS();

View File

@ -16,6 +16,7 @@ import {
IconLibrary, IconLibrary,
IconMenu, IconMenu,
IconNewItem, IconNewItem,
IconOSS,
IconOwner, IconOwner,
IconReader, IconReader,
IconReplace, IconReplace,
@ -29,6 +30,7 @@ import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
@ -36,6 +38,7 @@ import { AccessPolicy } from '@/models/library';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels'; import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import { useRSEdit } from './RSEditContext'; import { useRSEdit } from './RSEditContext';
interface MenuRSTabsProps { interface MenuRSTabsProps {
@ -47,6 +50,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const model = useRSForm(); const model = useRSForm();
const library = useLibrary();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
@ -181,6 +185,13 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
) : null} ) : null}
{library.globalOSS ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH))}
/>
) : null}
<DropdownButton <DropdownButton
text='Библиотека' text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />} icon={<IconLibrary size='1rem' className='icon-primary' />}

View File

@ -22,6 +22,7 @@ import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsfor
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelVersion, prompts } from '@/utils/labels'; import { information, labelVersion, prompts } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import EditorConstituenta from './EditorConstituenta'; import EditorConstituenta from './EditorConstituenta';
import EditorRSForm from './EditorRSFormCard'; import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList'; import EditorRSList from './EditorRSList';
@ -39,13 +40,13 @@ export enum RSTabID {
function RSTabs() { function RSTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID; const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
const version = query.get('v') ? Number(query.get('v')) : undefined; const version = query.get('v') ? Number(query.get('v')) : undefined;
const cstQuery = query.get('active'); const cstQuery = query.get('active');
const { setNoFooter, calculateHeight } = useConceptOptions(); const { setNoFooter, calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm(); const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const { destroyItem } = useLibrary(); const library = useLibrary();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified); useBlockNavigation(isModified);
@ -176,11 +177,16 @@ function RSTabs() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) { if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return; return;
} }
destroyItem(schema.id, () => { const backToOSS = library.globalOSS?.schemas.includes(schema.id);
library.destroyItem(schema.id, () => {
toast.success(information.itemDestroyed); toast.success(information.itemDestroyed);
if (backToOSS) {
router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH));
} else {
router.push(urls.library); router.push(urls.library);
}
}); });
}, [schema, destroyItem, router]); }, [schema, library, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);

View File

@ -1,6 +1,8 @@
/** /**
* Module: Override imported components CSS styling. * Module: Override imported components CSS styling.
*/ */
@import './constants.css';
@import './imports.css';
:root { :root {
/* Import overrides */ /* Import overrides */

View File

@ -54,7 +54,8 @@ export const patterns = {
export const resources = { export const resources = {
graph_font: '/DejaVu.ttf', graph_font: '/DejaVu.ttf',
privacy_policy: '/privacy.pdf', privacy_policy: '/privacy.pdf',
logo: '/logo_full.svg' logo: '/logo_full.svg',
db_schema: '/db_schema.svg'
}; };
/** /**
@ -114,6 +115,8 @@ export const storage = {
rsgraphFoldHidden: 'rsgraph.fold_hidden', rsgraphFoldHidden: 'rsgraph.fold_hidden',
ossShowGrid: 'oss.show_grid', ossShowGrid: 'oss.show_grid',
ossEdgeStraight: 'oss.edge_straight',
ossEdgeAnimate: 'oss.edge_animate',
cstFilterMatch: 'cst.filter.match', cstFilterMatch: 'cst.filter.match',
cstFilterGraph: 'cst.filter.graph' cstFilterGraph: 'cst.filter.graph'