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",
"SIDELIST",
"signup",
"simplebezier",
"Slng",
"SMALLPR",
"Stylesheet",

View File

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

View File

@ -4,8 +4,8 @@ from .basics import OperationPositionSerializer, PositionsSerializer, Substituti
from .data_access import (
ArgumentSerializer,
OperationCreateSerializer,
OperationDeleteSerializer,
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. '''
target = PKField(many=False, queryset=Operation.objects.all())
positions = serializers.ListField(

View File

@ -1,6 +1,8 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer
from .data_access import OperationSchemaSerializer, OperationSerializer
@ -8,3 +10,9 @@ class NewOperationResponse(serializers.Serializer):
''' Serializer: Create operation response. '''
new_operation = OperationSerializer()
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')
def test_create_operation(self):
self.populateData()
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.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')
def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id)
@ -269,3 +250,40 @@ class TestOssViewset(EndpointTester):
self.login()
response = self.executeOK(data=data)
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 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 viewsets
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.serializers import LibraryItemSerializer
from shared import messages as msg
from shared import permissions
from .. import models as m
@ -33,7 +34,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if self.action in [
'create_operation',
'delete_operation',
'update_positions'
'update_positions',
'create_input'
]:
permission_list = [permissions.ItemEditor]
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.refresh_from_db()
response = Response(
return Response(
status=c.HTTP_201_CREATED,
data={
'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)
return response
@extend_schema(
summary='delete operation',
tags=['OSS'],
request=s.OperationDeleteSerializer,
request=s.OperationTargetSerializer,
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
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')
def delete_operation(self, request: Request, pk):
''' Endpoint: Delete operation. '''
serializer = s.OperationDeleteSerializer(
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
@ -156,3 +157,59 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
status=c.HTTP_200_OK,
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 'Нет доступа к схеме'
def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}'
def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}'
def renameTrivial(name: str):
return f'Имя должно отличаться от текущего: {name}'

View File

@ -29,6 +29,7 @@
"react-tabs": "^6.0.2",
"react-toastify": "^10.0.5",
"react-tooltip": "^5.27.1",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"reagraph": "^4.19.2",
"use-debounce": "^10.0.1"
@ -10542,6 +10543,20 @@
"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": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",

View File

@ -33,6 +33,7 @@
"react-tabs": "^6.0.2",
"react-toastify": "^10.0.5",
"react-tooltip": "^5.27.1",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"reagraph": "^4.19.2",
"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,
IconDarkTheme,
IconDatabase,
IconDBStructure,
IconHelp,
IconHelpOff,
IconImage,
IconLightTheme,
IconLogout,
IconRESTapi,
IconUser
} from '@/components/Icons';
import { CProps } from '@/components/props';
@ -43,6 +46,21 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
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() {
hideDropdown();
toggleDarkMode();
@ -77,7 +95,34 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
/>
) : null}
{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}
<DropdownButton
text='Выйти...'

View File

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

View File

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

View File

@ -3,6 +3,7 @@
*/
import {
IInputCreatedResponse,
IOperationCreateData,
IOperationCreatedResponse,
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({
endpoint: `/api/oss/${schema}/update-positions`,
endpoint: `/api/oss/${oss}/update-positions`,
request: request
});
}
export function postCreateOperation(
schema: string,
oss: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) {
AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`,
endpoint: `/api/oss/${oss}/create-operation`,
request: request
});
}
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
export function patchDeleteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
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
});
}

View File

@ -38,7 +38,6 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { LuLightbulb as IconHelp } 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 { RiUnpinLine as IconUnpin } from 'react-icons/ri';
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 { IoLibrary as IconLibrary2 } from 'react-icons/io5';
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 { LuArchive as IconArchive } 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 { TbColumns as IconList } from 'react-icons/tb';
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 { LuBookCopy as IconInlineSynthesis } 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 =======
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 { LuSparkles as IconClustering } 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 ======
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[];
value?: IConstituenta;
onSelectValue: (newValue?: IConstituenta) => void;
placeholder?: string;
noBorder?: boolean;
}
function SelectConstituenta({

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
'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 {
@ -12,12 +12,18 @@ import {
patchSetOwner,
postSubscribe
} 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 useOssDetails from '@/hooks/useOssDetails';
import { AccessPolicy, ILibraryItem } 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 { contextOutsideScope } from '@/utils/labels';
@ -48,6 +54,7 @@ interface IOssContext {
savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
}
const OssContext = createContext<IOssContext | null>(null);
@ -66,13 +73,8 @@ interface OssStateProps {
export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary();
const schema = library.globalOSS;
const { user } = useAuth();
const {
schema, // prettier: split lines
error: errorLoading,
setSchema,
loading
} = useOssDetails({ target: itemID });
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
@ -90,6 +92,12 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, schema, toggleTracking]);
useEffect(() => {
if (schema?.id !== Number(itemID)) {
library.setGlobalID(itemID);
}
}, [itemID, schema, library]);
const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) {
@ -102,13 +110,14 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
setSchema(Object.assign(schema, newData));
const fullData: IOperationSchemaData = Object.assign(schema, newData);
library.setGlobalOSS(fullData);
library.localUpdateItem(newData);
if (callback) callback(newData);
}
});
},
[itemID, setSchema, schema, library]
[itemID, schema, library]
);
const subscribe = useCallback(
@ -133,7 +142,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
}
});
},
[itemID, schema, user]
[itemID, user, schema]
);
const unsubscribe = useCallback(
@ -278,13 +287,13 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
setSchema(newData.oss);
library.setGlobalOSS(newData.oss);
library.localUpdateTimestamp(newData.oss.id);
if (callback) callback(newData.new_operation);
}
});
},
[itemID, library, setSchema]
[itemID, library]
);
const deleteOperation = useCallback(
@ -296,13 +305,32 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
setSchema(newData);
library.setGlobalOSS(newData);
library.localUpdateTimestamp(newData.id);
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 (
@ -310,8 +338,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
value={{
schema,
itemID,
loading,
errorLoading,
loading: library.ossLoading,
errorLoading: library.ossError,
processing,
processingError,
isOwned,
@ -327,7 +355,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
savePositions,
createOperation,
deleteOperation
deleteOperation,
createInput
}}
>
{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 Label from '@/components/ui/Label';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
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 PickMultiOperation from '../../components/select/PickMultiOperation';
interface TabSynthesisOperationProps {
oss: IOperationSchema;
alias: string;
@ -34,22 +31,6 @@ function TabSynthesisOperation({
inputs,
setInputs
}: 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 (
<AnimateFade className='cc-column'>
<TextInput
@ -79,16 +60,10 @@ function TabSynthesisOperation({
/>
</div>
<div className='flex justify-between'>
<FlexColumn>
<Label text='Аргумент 1' />
<SelectOperation items={oss.items} value={left} onSelectValue={setLeft} />
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={oss.items} selected={inputs} setSelected={setInputs} rows={6} />
</FlexColumn>
<FlexColumn>
<Label text='Аргумент 2' className='text-right' />
<SelectOperation items={oss.items} value={right} onSelectValue={setRight} />
</FlexColumn>
</div>
</AnimateFade>
);
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@
*/
import { Graph } from './Graph';
import { ILibraryItemData, LibraryItemID } from './library';
import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library';
import { ConstituentaID } from './rsform';
/**
@ -139,3 +139,11 @@ export interface IOperationCreatedResponse {
new_operation: IOperation;
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';
export function IconsPage() {
const iconsList = Object.keys(icons).filter(key => key.startsWith('Icon'));
return (
<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'>
{Object.keys(icons)
.filter(key => key.startsWith('Icon'))
.map((key, index) => (
{iconsList.map((key, index) => (
<div key={`icons_list_${index}`} className='flex flex-col items-center px-3 pb-6'>
<p>{icons[key]({ size: '2rem' })}</p>
<p>{key}</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,18 @@
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 MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous';
@ -12,6 +24,8 @@ import { useOssEdit } from '../OssEditContext';
interface ToolbarOssGraphProps {
isModified: boolean;
showGrid: boolean;
edgeAnimate: boolean;
edgeStraight: boolean;
onCreate: () => void;
onDelete: () => void;
onFitView: () => void;
@ -19,39 +33,30 @@ interface ToolbarOssGraphProps {
onSavePositions: () => void;
onResetPositions: () => void;
toggleShowGrid: () => void;
toggleEdgeAnimate: () => void;
toggleEdgeStraight: () => void;
}
function ToolbarOssGraph({
isModified,
showGrid,
edgeAnimate,
edgeStraight,
onCreate,
onDelete,
onFitView,
onSaveImage,
onSavePositions,
onResetPositions,
toggleShowGrid
toggleShowGrid,
toggleEdgeAnimate,
toggleEdgeStraight
}: ToolbarOssGraphProps) {
const controller = useOssEdit();
return (
<div className='flex flex-col items-center'>
<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
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Сбросить вид'
@ -68,22 +73,28 @@ function ToolbarOssGraph({
}
onClick={toggleShowGrid}
/>
{controller.isMutable ? (
<MiniButton
title='Новая операция'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={onCreate}
title={edgeStraight ? 'Связи: прямые' : 'Связи: безье'}
icon={
edgeStraight ? (
<IconLineStraight size='1.25rem' className='icon-primary' />
) : (
<IconLineWave size='1.25rem' className='icon-primary' />
)
}
onClick={toggleEdgeStraight}
/>
) : null}
{controller.isMutable ? (
<MiniButton
title='Удалить выбранную'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.selected.length !== 1 || controller.isProcessing}
onClick={onDelete}
title={edgeAnimate ? 'Анимация: вкл' : 'Анимация: выкл'}
icon={
edgeAnimate ? (
<IconAnimation size='1.25rem' className='icon-primary' />
) : (
<IconAnimationOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleEdgeAnimate}
/>
) : null}
<MiniButton
icon={<IconImage size='1.25rem' className='icon-primary' />}
title='Сохранить изображение'
@ -95,6 +106,36 @@ function ToolbarOssGraph({
offset={4}
/>
</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;
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
}
const OssEditContext = createContext<IOssEditContext | null>(null);
@ -210,6 +211,16 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[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 (
<OssEditContext.Provider
value={{
@ -234,7 +245,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
openOperationSchema,
savePositions,
promptCreateOperation,
deleteOperation
deleteOperation,
createInput
}}
>
{model.schema ? (

View File

@ -33,7 +33,7 @@ export enum OssTabID {
function OssTabs() {
const router = useConceptNavigation();
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 { schema, loading, errorLoading } = useOSS();

View File

@ -16,6 +16,7 @@ import {
IconLibrary,
IconMenu,
IconNewItem,
IconOSS,
IconOwner,
IconReader,
IconReplace,
@ -29,6 +30,7 @@ import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import useDropdown from '@/hooks/useDropdown';
@ -36,6 +38,7 @@ import { AccessPolicy } from '@/models/library';
import { UserLevel } from '@/models/user';
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import { useRSEdit } from './RSEditContext';
interface MenuRSTabsProps {
@ -47,6 +50,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
const router = useConceptNavigation();
const { user } = useAuth();
const model = useRSForm();
const library = useLibrary();
const { accessLevel, setAccessLevel } = useAccessMode();
@ -181,6 +185,13 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew}
/>
) : null}
{library.globalOSS ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH))}
/>
) : null}
<DropdownButton
text='Библиотека'
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 { information, labelVersion, prompts } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import EditorConstituenta from './EditorConstituenta';
import EditorRSForm from './EditorRSFormCard';
import EditorRSList from './EditorRSList';
@ -39,13 +40,13 @@ export enum RSTabID {
function RSTabs() {
const router = useConceptNavigation();
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 cstQuery = query.get('active');
const { setNoFooter, calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const { destroyItem } = useLibrary();
const library = useLibrary();
const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified);
@ -176,11 +177,16 @@ function RSTabs() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return;
}
destroyItem(schema.id, () => {
const backToOSS = library.globalOSS?.schemas.includes(schema.id);
library.destroyItem(schema.id, () => {
toast.success(information.itemDestroyed);
if (backToOSS) {
router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH));
} else {
router.push(urls.library);
}
});
}, [schema, destroyItem, router]);
}, [schema, library, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);

View File

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

View File

@ -54,7 +54,8 @@ export const patterns = {
export const resources = {
graph_font: '/DejaVu.ttf',
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',
ossShowGrid: 'oss.show_grid',
ossEdgeStraight: 'oss.edge_straight',
ossEdgeAnimate: 'oss.edge_animate',
cstFilterMatch: 'cst.filter.match',
cstFilterGraph: 'cst.filter.graph'