F: Improve RSForm and OSS UI + some fixes

This commit is contained in:
Ivan 2024-08-01 20:11:32 +03:00
parent 5a4edafcb5
commit 10a4140c95
27 changed files with 303 additions and 93 deletions

View File

@ -1,13 +1,11 @@
''' Models: OSS API. ''' ''' Models: OSS API. '''
from typing import Optional from typing import Optional
from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from apps.library.models import Editor, LibraryItem, LibraryItemType from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from shared import messages as msg
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance from .Inheritance import Inheritance
@ -66,8 +64,6 @@ class OperationSchema:
@transaction.atomic @transaction.atomic
def create_operation(self, **kwargs) -> Operation: def create_operation(self, **kwargs) -> Operation:
''' Insert new operation. ''' ''' Insert new operation. '''
if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists():
raise ValidationError(msg.aliasTaken(kwargs['alias']))
result = Operation.objects.create(oss=self.model, **kwargs) result = Operation.objects.create(oss=self.model, **kwargs)
self.save() self.save()
result.refresh_from_db() result.refresh_from_db()

View File

@ -1,13 +1,66 @@
'use client';
import { createColumnHelper } from '@tanstack/react-table';
import { useMemo } from 'react';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import { OssNodeInternal } from '@/models/miscellaneous'; import { OssNodeInternal } from '@/models/miscellaneous';
import { ICstSubstituteEx } from '@/models/oss';
import { labelOperationType } from '@/utils/labels'; import { labelOperationType } from '@/utils/labels';
import { IconPageRight } from '../Icons';
import DataTable from '../ui/DataTable';
interface TooltipOperationProps { interface TooltipOperationProps {
node: OssNodeInternal; node: OssNodeInternal;
anchor: string; anchor: string;
} }
const columnHelper = createColumnHelper<ICstSubstituteEx>();
function TooltipOperation({ node, anchor }: TooltipOperationProps) { function TooltipOperation({ node, anchor }: TooltipOperationProps) {
const columns = useMemo(
() => [
columnHelper.accessor('substitution_term', {
id: 'substitution_term',
size: 200
}),
columnHelper.accessor('substitution_alias', {
id: 'substitution_alias',
size: 50
}),
columnHelper.display({
id: 'status',
header: '',
size: 40,
cell: () => <IconPageRight size='1.2rem' />
}),
columnHelper.accessor('original_alias', {
id: 'original_alias',
size: 50
}),
columnHelper.accessor('original_term', {
id: 'original_term',
size: 200
})
],
[]
);
const table = useMemo(
() => (
<DataTable
dense
noHeader
noFooter
className='w-full text-sm border select-none mb-2'
data={node.data.operation.substitutions}
columns={columns}
/>
),
[columns, node]
);
return ( return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'> <Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'>
<h2>{node.data.operation.alias}</h2> <h2>{node.data.operation.alias}</h2>
@ -29,6 +82,7 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
<p> <p>
<b>Положение:</b> [{node.xPos}, {node.yPos}] <b>Положение:</b> [{node.xPos}, {node.yPos}]
</p> </p>
{node.data.operation.substitutions.length > 0 ? table : null}
</Tooltip> </Tooltip>
); );
} }

View File

@ -0,0 +1,43 @@
'use client';
import { IconOSS } from '@/components/Icons';
import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemReference } from '@/models/library';
import { prefixes } from '@/utils/constants';
interface MiniSelectorOSSProps {
items: ILibraryItemReference[];
onSelect: (event: CProps.EventMouse, newValue: ILibraryItemReference) => void;
}
function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) {
const ossMenu = useDropdown();
return (
<div ref={ossMenu.ref} className='flex items-center'>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
title='Связанные операционные схемы'
hideTitle={ossMenu.isOpen}
onClick={() => ossMenu.toggle()}
/>
<Dropdown isOpen={ossMenu.isOpen}>
<Label text='Список ОСС' className='border-b px-3 py-1' />
{items.map((reference, index) => (
<DropdownButton
className='min-w-[5rem]'
key={`${prefixes.oss_list}${index}`}
text={reference.alias}
onClick={event => onSelect(event, reference)}
/>
))}
</Dropdown>
</div>
);
}
export default MiniSelectorOSS;

View File

@ -25,8 +25,8 @@ import { type ErrorData } from '@/components/info/InfoError';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
import { import {
IOperation,
IOperationCreateData, IOperationCreateData,
IOperationData,
IOperationSchema, IOperationSchema,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData, IOperationSetInputData,
@ -62,7 +62,7 @@ interface IOssContext {
setEditors: (newEditors: UserID[], callback?: () => void) => void; setEditors: (newEditors: UserID[], callback?: () => void) => void;
savePositions: (data: IPositionsData, callback?: () => void) => void; savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperationData>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void; createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
setInput: (data: IOperationSetInputData, callback?: () => void) => void; setInput: (data: IOperationSetInputData, callback?: () => void) => void;
@ -292,7 +292,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
); );
const createOperation = useCallback( const createOperation = useCallback(
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => { (data: IOperationCreateData, callback?: DataCallback<IOperationData>) => {
setProcessingError(undefined); setProcessingError(undefined);
postCreateOperation(itemID, { postCreateOperation(itemID, {
data: data, data: data,

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -22,6 +22,7 @@ interface DlgCreateOperationProps {
hideWindow: () => void; hideWindow: () => void;
oss: IOperationSchema; oss: IOperationSchema;
onCreate: (data: IOperationCreateData) => void; onCreate: (data: IOperationCreateData) => void;
initialInputs: OperationID[];
} }
export enum TabID { export enum TabID {
@ -29,21 +30,31 @@ export enum TabID {
SYNTHESIS = 1 SYNTHESIS = 1
} }
function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationProps) { function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCreateOperationProps) {
const library = useLibrary(); const library = useLibrary();
const [activeTab, setActiveTab] = useState(TabID.INPUT); const [activeTab, setActiveTab] = useState(initialInputs.length > 0 ? TabID.SYNTHESIS : TabID.INPUT);
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [inputs, setInputs] = useState<OperationID[]>([]); const [inputs, setInputs] = useState<OperationID[]>(initialInputs);
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined); const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [createSchema, setCreateSchema] = useState(false); const [createSchema, setCreateSchema] = useState(false);
const isValid = useMemo( const isValid = useMemo(() => {
() => (alias !== '' && activeTab === TabID.INPUT) || inputs.length != 1, if (alias === '') {
[alias, activeTab, inputs] return false;
); }
if (activeTab === TabID.SYNTHESIS && inputs.length === 1) {
return false;
}
if (activeTab === TabID.INPUT && !attachedID) {
if (oss.items.some(operation => operation.alias === alias)) {
return false;
}
}
return true;
}, [alias, activeTab, inputs, attachedID, oss.items]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (attachedID) { if (attachedID) {
@ -74,6 +85,21 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
onCreate(data); onCreate(data);
}; };
const handleSelectTab = useCallback(
(newTab: TabID, last: TabID) => {
if (last === newTab) {
return;
}
if (newTab === TabID.INPUT) {
setAttachedID(undefined);
} else {
setInputs(initialInputs);
}
setActiveTab(newTab);
},
[setActiveTab, initialInputs]
);
const inputPanel = useMemo( const inputPanel = useMemo(
() => ( () => (
<TabPanel> <TabPanel>
@ -131,7 +157,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
className='flex flex-col' className='flex flex-col'
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={handleSelectTab}
> >
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}> <TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
<TabLabel <TabLabel

View File

@ -50,7 +50,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
() => inputOperations.map(operation => operation.result).filter(id => id !== null), () => inputOperations.map(operation => operation.result).filter(id => id !== null),
[inputOperations] [inputOperations]
); );
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(oss.substitutions); const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(target.substitutions);
const cache = useRSFormCache(); const cache = useRSFormCache();
const schemas = useMemo( const schemas = useMemo(
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined), () => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),

View File

@ -32,6 +32,7 @@ export class OssLoader {
this.prepareLookups(); this.prepareLookups();
this.createGraph(); this.createGraph();
this.extractSchemas(); this.extractSchemas();
this.inferOperationAttributes();
result.operationByID = this.operationByID; result.operationByID = this.operationByID;
result.graph = this.graph; result.graph = this.graph;
@ -42,7 +43,7 @@ export class OssLoader {
private prepareLookups() { private prepareLookups() {
this.oss.items.forEach(operation => { this.oss.items.forEach(operation => {
this.operationByID.set(operation.id, operation); this.operationByID.set(operation.id, operation as IOperation);
this.graph.addNode(operation.id); this.graph.addNode(operation.id);
}); });
} }
@ -55,6 +56,16 @@ export class OssLoader {
this.schemas = this.oss.items.map(operation => operation.result as LibraryItemID).filter(item => item !== null); this.schemas = this.oss.items.map(operation => operation.result as LibraryItemID).filter(item => item !== null);
} }
private inferOperationAttributes() {
this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!;
operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
operation.arguments = this.oss.arguments
.filter(item => item.operation === operationID)
.map(item => item.argument);
});
}
private calculateStats(): IOperationSchemaStats { private calculateStats(): IOperationSchemaStats {
const items = this.oss.items; const items = this.oss.items;
return { return {

View File

@ -35,8 +35,16 @@ export interface IOperation {
position_y: number; position_y: number;
result: LibraryItemID | null; result: LibraryItemID | null;
substitutions: ICstSubstituteEx[];
arguments: OperationID[];
} }
/**
* Represents {@link IOperation} data from server.
*/
export interface IOperationData extends Omit<IOperation, 'substitutions' | 'arguments'> {}
/** /**
* Represents {@link IOperation} position. * Represents {@link IOperation} position.
*/ */
@ -121,6 +129,7 @@ export interface IMultiSubstitution {
* Represents {@link ICstSubstitute} extended data. * Represents {@link ICstSubstitute} extended data.
*/ */
export interface ICstSubstituteEx extends ICstSubstitute { export interface ICstSubstituteEx extends ICstSubstitute {
operation: OperationID;
original_alias: string; original_alias: string;
original_term: string; original_term: string;
substitution_alias: string; substitution_alias: string;
@ -141,7 +150,7 @@ export interface IOperationSchemaStats {
* Represents backend data for {@link IOperationSchema}. * Represents backend data for {@link IOperationSchema}.
*/ */
export interface IOperationSchemaData extends ILibraryItemData { export interface IOperationSchemaData extends ILibraryItemData {
items: IOperation[]; items: IOperationData[];
arguments: IArgument[]; arguments: IArgument[];
substitutions: ICstSubstituteEx[]; substitutions: ICstSubstituteEx[];
} }
@ -150,6 +159,7 @@ export interface IOperationSchemaData extends ILibraryItemData {
* Represents OperationSchema. * Represents OperationSchema.
*/ */
export interface IOperationSchema extends IOperationSchemaData { export interface IOperationSchema extends IOperationSchemaData {
items: IOperation[];
graph: Graph; graph: Graph;
schemas: LibraryItemID[]; schemas: LibraryItemID[];
stats: IOperationSchemaStats; stats: IOperationSchemaStats;
@ -160,7 +170,7 @@ export interface IOperationSchema extends IOperationSchemaData {
* Represents data response when creating {@link IOperation}. * Represents data response when creating {@link IOperation}.
*/ */
export interface IOperationCreatedResponse { export interface IOperationCreatedResponse {
new_operation: IOperation; new_operation: IOperationData;
oss: IOperationSchemaData; oss: IOperationSchemaData;
} }

View File

@ -91,7 +91,7 @@ export interface ITargetCst {
} }
/** /**
* Represents Constituenta data from server. * Represents {@link IConstituenta} data from server.
*/ */
export interface IConstituentaData extends IConstituentaMeta { export interface IConstituentaData extends IConstituentaMeta {
parse: { parse: {

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -22,10 +22,11 @@ import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI'; import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { EXTEOR_TRS_FILE, limits, patterns } from '@/utils/constants'; import { EXTEOR_TRS_FILE, limits, patterns, storage } from '@/utils/constants';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
function FormCreateItem() { function FormCreateItem() {
@ -45,6 +46,7 @@ function FormCreateItem() {
const location = useMemo(() => combineLocation(head, body), [head, body]); const location = useMemo(() => combineLocation(head, body), [head, body]);
const isValid = useMemo(() => validateLocation(location), [location]); const isValid = useMemo(() => validateLocation(location), [location]);
const [initLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
const [file, setFile] = useState<File | undefined>(); const [file, setFile] = useState<File | undefined>();
@ -104,6 +106,13 @@ function FormCreateItem() {
setBody(newValue.length > 3 ? newValue.substring(3) : ''); setBody(newValue.length > 3 ? newValue.substring(3) : '');
}, []); }, []);
useLayoutEffect(() => {
if (!initLocation) {
return;
}
handleSelectLocation(initLocation);
}, [initLocation, handleSelectLocation]);
return ( return (
<form className={clsx('cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')} onSubmit={handleSubmit}> <form className={clsx('cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')} onSubmit={handleSubmit}>
<h1> <h1>

View File

@ -7,6 +7,7 @@ import {
IconMoveDown, IconMoveDown,
IconMoveUp, IconMoveUp,
IconNewItem, IconNewItem,
IconOSS,
IconReset, IconReset,
IconSave, IconSave,
IconStatusOK, IconStatusOK,
@ -22,6 +23,9 @@ function HelpCstEditor() {
return ( return (
<div className='dense'> <div className='dense'>
<h1>Редактор конституенты</h1> <h1>Редактор конституенты</h1>
<li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li> <li>
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S <IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
</li> </li>

View File

@ -5,6 +5,7 @@ import {
IconEditor, IconEditor,
IconFollow, IconFollow,
IconImmutable, IconImmutable,
IconOSS,
IconOwner, IconOwner,
IconPublic, IconPublic,
IconSave IconSave
@ -29,6 +30,9 @@ function HelpRSFormCard() {
</p> </p>
<h2>Управление</h2> <h2>Управление</h2>
<li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li> <li>
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S <IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
</li> </li>

View File

@ -6,6 +6,7 @@ import {
IconMoveUp, IconMoveUp,
IconNewItem, IconNewItem,
IconOpenList, IconOpenList,
IconOSS,
IconReset IconReset
} from '@/components/Icons'; } from '@/components/Icons';
import InfoCstStatus from '@/components/info/InfoCstStatus'; import InfoCstStatus from '@/components/info/InfoCstStatus';
@ -27,6 +28,9 @@ function HelpRSFormItems() {
</li> </li>
<h2>Управление списком</h2> <h2>Управление списком</h2>
<li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li> <li>
<IconReset className='inline-icon' /> сбросить выделение: ESC <IconReset className='inline-icon' /> сбросить выделение: ESC
</li> </li>

View File

@ -12,6 +12,7 @@ import {
IconGraphOutputs, IconGraphOutputs,
IconImage, IconImage,
IconNewItem, IconNewItem,
IconOSS,
IconReset, IconReset,
IconRotate3D, IconRotate3D,
IconText IconText
@ -70,6 +71,9 @@ function HelpTermGraph() {
<div className='flex flex-col-reverse mb-3 sm:flex-row'> <div className='flex flex-col-reverse mb-3 sm:flex-row'>
<div className='w-full sm:w-[14rem]'> <div className='w-full sm:w-[14rem]'>
<h1>Общие</h1> <h1>Общие</h1>
<li>
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
</li>
<li> <li>
<IconFilter className='inline-icon' /> Открыть настройки <IconFilter className='inline-icon' /> Открыть настройки
</li> </li>

View File

@ -101,11 +101,11 @@ function NodeContextMenu({
}; };
return ( return (
<div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}> <div ref={ref} className='absolute select-none' 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('Редактировать операцию', 'Двойной клик')}
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleEditOperation} onClick={handleEditOperation}

View File

@ -122,10 +122,43 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
controller.savePositions(getPositions(), () => setIsModified(false)); controller.savePositions(getPositions(), () => setIsModified(false));
}, [controller, getPositions, setIsModified]); }, [controller, getPositions, setIsModified]);
const handleCreateOperation = useCallback(() => { const handleCreateOperation = useCallback(
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); (inputs: OperationID[]) => () => {
controller.promptCreateOperation(center.x, center.y, getPositions()); if (!controller.schema) {
}, [controller, getPositions, flow]); return;
}
let target = { x: 0, y: 0 };
const positions = getPositions();
if (inputs.length <= 1) {
target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
} else {
const inputsNodes = positions.filter(pos => inputs.includes(pos.id));
const maxY = Math.max(...inputsNodes.map(node => node.position_y));
const minX = Math.min(...inputsNodes.map(node => node.position_x));
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
target.y = maxY + 100;
target.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize;
}
let flagIntersect = false;
do {
flagIntersect = positions.some(
position =>
Math.abs(position.position_x - target.x) < PARAMETER.ossMinDistance &&
Math.abs(position.position_y - target.y) < PARAMETER.ossMinDistance
);
if (flagIntersect) {
target.x += PARAMETER.ossMinDistance;
target.y += PARAMETER.ossMinDistance;
}
} while (flagIntersect);
controller.promptCreateOperation(target.x, target.y, inputs, positions);
},
[controller, getPositions, flow]
);
const handleDeleteSelected = useCallback(() => { const handleDeleteSelected = useCallback(() => {
if (controller.selected.length !== 1) { if (controller.selected.length !== 1) {
@ -241,13 +274,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleContextMenuHide(); handleContextMenuHide();
}, [handleContextMenuHide]); }, [handleContextMenuHide]);
const handleNodeClick = useCallback( const handleNodeDoubleClick = useCallback(
(event: CProps.EventMouse, node: OssNode) => { (event: CProps.EventMouse, node: OssNode) => {
if (event.ctrlKey || event.metaKey) { event.preventDefault();
event.preventDefault(); event.stopPropagation();
event.stopPropagation(); handleEditOperation(Number(node.id));
handleEditOperation(Number(node.id));
}
}, },
[handleEditOperation] [handleEditOperation]
); );
@ -268,7 +299,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
if ((event.ctrlKey || event.metaKey) && event.key === 'q') { if ((event.ctrlKey || event.metaKey) && event.key === 'q') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleCreateOperation(); handleCreateOperation(controller.selected);
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
@ -297,7 +328,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
edges={edges} edges={edges}
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick}
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
fitView fitView
nodeTypes={OssNodeTypes} nodeTypes={OssNodeTypes}
@ -305,11 +336,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
minZoom={0.75} minZoom={0.75}
nodesConnectable={false} nodesConnectable={false}
snapToGrid={true} snapToGrid={true}
snapGrid={[10, 10]} snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
onNodeContextMenu={handleContextMenu} onNodeContextMenu={handleContextMenu}
onClick={handleClickCanvas} onClick={handleClickCanvas}
> >
{showGrid ? <Background gap={10} /> : null} {showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
</ReactFlow> </ReactFlow>
), ),
[ [
@ -319,7 +350,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleContextMenu, handleContextMenu,
handleClickCanvas, handleClickCanvas,
onEdgesChange, onEdgesChange,
handleNodeClick, handleNodeDoubleClick,
OssNodeTypes, OssNodeTypes,
showGrid showGrid
] ]
@ -334,7 +365,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
edgeAnimate={edgeAnimate} edgeAnimate={edgeAnimate}
edgeStraight={edgeStraight} edgeStraight={edgeStraight}
onFitView={handleFitView} onFitView={handleFitView}
onCreate={handleCreateOperation} onCreate={handleCreateOperation(controller.selected)}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onEdit={() => handleEditOperation(controller.selected[0])} onEdit={() => handleEditOperation(controller.selected[0])}
onExecute={handleExecuteSelected} onExecute={handleExecuteSelected}

View File

@ -167,7 +167,7 @@ function ToolbarOssGraph({
onClick={onExecute} onClick={onExecute}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Ctrl + клик')} titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />} icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
disabled={controller.selected.length !== 1 || controller.isProcessing} disabled={controller.selected.length !== 1 || controller.isProcessing}
onClick={onEdit} onClick={onEdit}

View File

@ -51,7 +51,7 @@ export interface IOssEditContext {
openOperationSchema: (target: OperationID) => void; openOperationSchema: (target: OperationID) => void;
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, inputs: OperationID[], positions: IOperationPosition[]) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
@ -96,6 +96,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const [showCreateOperation, setShowCreateOperation] = useState(false); const [showCreateOperation, setShowCreateOperation] = useState(false);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 }); const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
const [initialInputs, setInitialInputs] = useState<OperationID[]>([]);
const [positions, setPositions] = useState<IOperationPosition[]>([]); const [positions, setPositions] = useState<IOperationPosition[]>([]);
const [targetOperationID, setTargetOperationID] = useState<OperationID | undefined>(undefined); const [targetOperationID, setTargetOperationID] = useState<OperationID | undefined>(undefined);
const targetOperation = useMemo( const targetOperation = useMemo(
@ -208,11 +209,15 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model] [model]
); );
const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => { const promptCreateOperation = useCallback(
setInsertPosition({ x: x, y: y }); (x: number, y: number, inputs: OperationID[], positions: IOperationPosition[]) => {
setPositions(positions); setInsertPosition({ x: x, y: y });
setShowCreateOperation(true); setInitialInputs(inputs);
}, []); setPositions(positions);
setShowCreateOperation(true);
},
[]
);
const handleCreateOperation = useCallback( const handleCreateOperation = useCallback(
(data: IOperationCreateData) => { (data: IOperationCreateData) => {
@ -341,6 +346,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
hideWindow={() => setShowCreateOperation(false)} hideWindow={() => setShowCreateOperation(false)}
oss={model.schema} oss={model.schema}
onCreate={handleCreateOperation} onCreate={handleCreateOperation}
initialInputs={initialInputs}
/> />
) : null} ) : null}
{showEditInput ? ( {showEditInput ? (

View File

@ -12,6 +12,7 @@ import {
IconSave IconSave
} from '@/components/Icons'; } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -54,6 +55,12 @@ function ToolbarConstituenta({
return ( return (
<Overlay position='top-1 right-4' className='cc-icons sm:right-1/2 sm:translate-x-1/2'> <Overlay position='top-1 right-4' className='cc-icons sm:right-1/2 sm:translate-x-1/2'>
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}

View File

@ -1,21 +1,16 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { IconOSS, IconSave } from '@/components/Icons'; import { IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion'; import SelectVersion from '@/components/select/SelectVersion';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
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 useDropdown from '@/hooks/useDropdown';
import { ILibraryUpdateData, LibraryItemType } from '@/models/library'; import { ILibraryUpdateData, LibraryItemType } from '@/models/library';
import { limits, patterns, prefixes } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ToolbarItemAccess from './ToolbarItemAccess'; import ToolbarItemAccess from './ToolbarItemAccess';
@ -37,8 +32,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [readOnly, setReadOnly] = useState(false); const [readOnly, setReadOnly] = useState(false);
const ossMenu = useDropdown();
useEffect(() => { useEffect(() => {
if (!schema) { if (!schema) {
setIsModified(false); setIsModified(false);
@ -92,35 +85,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
controller.updateSchema(data); controller.updateSchema(data);
}; };
const ossSelector = useMemo(
() =>
schema && schema?.oss.length > 0 ? (
<Overlay position='left-[12.5rem] top-[-0.4rem]'>
<div ref={ossMenu.ref}>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
noHover
title='Связанные операционные схемы'
hideTitle={ossMenu.isOpen}
onClick={() => ossMenu.toggle()}
/>
<Dropdown isOpen={ossMenu.isOpen} className='mt-[-0.1rem]'>
<Label text='Список ОСС' className='border-b px-3' />
{schema.oss.map((reference, index) => (
<DropdownButton
className='min-w-[5rem]'
key={`${prefixes.oss_list}${index}`}
text={reference.alias}
onClick={event => controller.viewOSS(reference.id, event.ctrlKey || event.metaKey)}
/>
))}
</Dropdown>
</div>
</Overlay>
) : null,
[schema, ossMenu, controller]
);
return ( return (
<form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}> <form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}>
<TextInput <TextInput
@ -132,7 +96,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
/> />
{ossSelector}
<div className='flex justify-between w-full gap-3 mb-3'> <div className='flex justify-between w-full gap-3 mb-3'>
<TextInput <TextInput
id='schema_alias' id='schema_alias'

View File

@ -29,7 +29,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
); );
return ( return (
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex'> <Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
<Label text='Доступ' className='self-center select-none' /> <Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'> <div className='ml-auto cc-icons'>
<SelectAccessPolicy <SelectAccessPolicy

View File

@ -5,15 +5,19 @@ import { useMemo } from 'react';
import { SubscribeIcon } from '@/components/DomainIcons'; import { SubscribeIcon } from '@/components/DomainIcons';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons'; import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { AccessPolicy, ILibraryItemEditor } from '@/models/library'; import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { IRSForm } from '@/models/rsform';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip, tooltips } from '@/utils/labels'; import { prepareTooltip, tooltips } from '@/utils/labels';
import { IRSEditContext } from '../RSEditContext';
interface ToolbarRSFormCardProps { interface ToolbarRSFormCardProps {
modified: boolean; modified: boolean;
subscribed: boolean; subscribed: boolean;
@ -33,8 +37,26 @@ function ToolbarRSFormCard({
}: ToolbarRSFormCardProps) { }: ToolbarRSFormCardProps) {
const { accessLevel } = useAccessMode(); const { accessLevel } = useAccessMode();
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]); const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
const ossSelector = useMemo(() => {
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
return null;
}
const schema = controller.schema as IRSForm;
if (schema.oss.length <= 0) {
return null;
}
return (
<MiniSelectorOSS
items={schema.oss}
onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)}
/>
);
}, [controller]);
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
{ossSelector}
{controller.isMutable || modified ? ( {controller.isMutable || modified ? (
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}

View File

@ -14,7 +14,7 @@ interface ToolbarVersioningProps {
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) { function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
const controller = useRSEdit(); const controller = useRSEdit();
return ( return (
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons'> <Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
{controller.isMutable ? ( {controller.isMutable ? (
<> <>
<MiniButton <MiniButton

View File

@ -8,6 +8,7 @@ import {
IconReset IconReset
} from '@/components/Icons'; } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
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 MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -27,6 +28,12 @@ function ToolbarRSList() {
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='items-start cc-icons'>
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сбросить выделение', 'ESC')} titleHtml={prepareTooltip('Сбросить выделение', 'ESC')}
icon={<IconReset size='1.25rem' className='icon-primary' />} icon={<IconReset size='1.25rem' className='icon-primary' />}

View File

@ -13,6 +13,7 @@ import {
IconTextOff IconTextOff
} from '@/components/Icons'; } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
@ -55,6 +56,12 @@ function ToolbarTermGraph({
return ( return (
<div className='cc-icons'> <div className='cc-icons'>
{controller.schema && controller.schema?.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
/>
) : null}
<MiniButton <MiniButton
title='Настройки фильтрации узлов и связей' title='Настройки фильтрации узлов и связей'
icon={<IconFilter size='1.25rem' className='icon-primary' />} icon={<IconFilter size='1.25rem' className='icon-primary' />}

View File

@ -163,9 +163,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
/> />
{controller.isContentEditable ? ( {controller.isContentEditable ? (
<DropdownButton <DropdownButton
text='Загрузить из Экстеора' text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />} icon={<IconUpload size='1rem' className='icon-red' />}
disabled={controller.isProcessing} disabled={controller.isProcessing || controller.schema?.oss.length !== 0}
onClick={handleUpload} onClick={handleUpload}
/> />
) : null} ) : null}

View File

@ -15,6 +15,8 @@ export const PARAMETER = {
ossImageWidth: 1280, // pixels - size of OSS image ossImageWidth: 1280, // pixels - size of OSS image
ossImageHeight: 960, // pixels - size of OSS image ossImageHeight: 960, // pixels - size of OSS image
ossContextMenuWidth: 200, // pixels - width of OSS context menu ossContextMenuWidth: 200, // pixels - width of OSS context menu
ossGridSize: 10, // pixels - size of OSS grid
ossMinDistance: 20, // pixels - minimum distance between node centers
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be