mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Improve RSForm and OSS UI + some fixes
This commit is contained in:
parent
5a4edafcb5
commit
10a4140c95
|
@ -1,13 +1,11 @@
|
|||
''' Models: OSS API. '''
|
||||
from typing import Optional
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from apps.library.models import Editor, LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import RSForm
|
||||
from shared import messages as msg
|
||||
|
||||
from .Argument import Argument
|
||||
from .Inheritance import Inheritance
|
||||
|
@ -66,8 +64,6 @@ class OperationSchema:
|
|||
@transaction.atomic
|
||||
def create_operation(self, **kwargs) -> 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)
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
|
|
|
@ -1,13 +1,66 @@
|
|||
'use client';
|
||||
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Tooltip from '@/components/ui/Tooltip';
|
||||
import { OssNodeInternal } from '@/models/miscellaneous';
|
||||
import { ICstSubstituteEx } from '@/models/oss';
|
||||
import { labelOperationType } from '@/utils/labels';
|
||||
|
||||
import { IconPageRight } from '../Icons';
|
||||
import DataTable from '../ui/DataTable';
|
||||
|
||||
interface TooltipOperationProps {
|
||||
node: OssNodeInternal;
|
||||
anchor: string;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ICstSubstituteEx>();
|
||||
|
||||
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 (
|
||||
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense my-3'>
|
||||
<h2>{node.data.operation.alias}</h2>
|
||||
|
@ -29,6 +82,7 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {
|
|||
<p>
|
||||
<b>Положение:</b> [{node.xPos}, {node.yPos}]
|
||||
</p>
|
||||
{node.data.operation.substitutions.length > 0 ? table : null}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
43
rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx
Normal file
43
rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx
Normal 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;
|
|
@ -25,8 +25,8 @@ import { type ErrorData } from '@/components/info/InfoError';
|
|||
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
||||
import { ILibraryUpdateData } from '@/models/library';
|
||||
import {
|
||||
IOperation,
|
||||
IOperationCreateData,
|
||||
IOperationData,
|
||||
IOperationSchema,
|
||||
IOperationSchemaData,
|
||||
IOperationSetInputData,
|
||||
|
@ -62,7 +62,7 @@ interface IOssContext {
|
|||
setEditors: (newEditors: UserID[], 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;
|
||||
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
|
||||
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
|
||||
|
@ -292,7 +292,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
);
|
||||
|
||||
const createOperation = useCallback(
|
||||
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => {
|
||||
(data: IOperationCreateData, callback?: DataCallback<IOperationData>) => {
|
||||
setProcessingError(undefined);
|
||||
postCreateOperation(itemID, {
|
||||
data: data,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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 BadgeHelp from '@/components/info/BadgeHelp';
|
||||
|
@ -22,6 +22,7 @@ interface DlgCreateOperationProps {
|
|||
hideWindow: () => void;
|
||||
oss: IOperationSchema;
|
||||
onCreate: (data: IOperationCreateData) => void;
|
||||
initialInputs: OperationID[];
|
||||
}
|
||||
|
||||
export enum TabID {
|
||||
|
@ -29,21 +30,31 @@ export enum TabID {
|
|||
SYNTHESIS = 1
|
||||
}
|
||||
|
||||
function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationProps) {
|
||||
function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCreateOperationProps) {
|
||||
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 [title, setTitle] = useState('');
|
||||
const [comment, setComment] = useState('');
|
||||
const [inputs, setInputs] = useState<OperationID[]>([]);
|
||||
const [inputs, setInputs] = useState<OperationID[]>(initialInputs);
|
||||
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
|
||||
const [createSchema, setCreateSchema] = useState(false);
|
||||
|
||||
const isValid = useMemo(
|
||||
() => (alias !== '' && activeTab === TabID.INPUT) || inputs.length != 1,
|
||||
[alias, activeTab, inputs]
|
||||
);
|
||||
const isValid = useMemo(() => {
|
||||
if (alias === '') {
|
||||
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(() => {
|
||||
if (attachedID) {
|
||||
|
@ -74,6 +85,21 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
|
|||
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(
|
||||
() => (
|
||||
<TabPanel>
|
||||
|
@ -131,7 +157,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
|
|||
selectedTabClassName='clr-selected'
|
||||
className='flex flex-col'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={setActiveTab}
|
||||
onSelect={handleSelectTab}
|
||||
>
|
||||
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
|
||||
<TabLabel
|
||||
|
|
|
@ -50,7 +50,7 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
() => inputOperations.map(operation => operation.result).filter(id => id !== null),
|
||||
[inputOperations]
|
||||
);
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(oss.substitutions);
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(target.substitutions);
|
||||
const cache = useRSFormCache();
|
||||
const schemas = useMemo(
|
||||
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),
|
||||
|
|
|
@ -32,6 +32,7 @@ export class OssLoader {
|
|||
this.prepareLookups();
|
||||
this.createGraph();
|
||||
this.extractSchemas();
|
||||
this.inferOperationAttributes();
|
||||
|
||||
result.operationByID = this.operationByID;
|
||||
result.graph = this.graph;
|
||||
|
@ -42,7 +43,7 @@ export class OssLoader {
|
|||
|
||||
private prepareLookups() {
|
||||
this.oss.items.forEach(operation => {
|
||||
this.operationByID.set(operation.id, operation);
|
||||
this.operationByID.set(operation.id, operation as IOperation);
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const items = this.oss.items;
|
||||
return {
|
||||
|
|
|
@ -35,8 +35,16 @@ export interface IOperation {
|
|||
position_y: number;
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -121,6 +129,7 @@ export interface IMultiSubstitution {
|
|||
* Represents {@link ICstSubstitute} extended data.
|
||||
*/
|
||||
export interface ICstSubstituteEx extends ICstSubstitute {
|
||||
operation: OperationID;
|
||||
original_alias: string;
|
||||
original_term: string;
|
||||
substitution_alias: string;
|
||||
|
@ -141,7 +150,7 @@ export interface IOperationSchemaStats {
|
|||
* Represents backend data for {@link IOperationSchema}.
|
||||
*/
|
||||
export interface IOperationSchemaData extends ILibraryItemData {
|
||||
items: IOperation[];
|
||||
items: IOperationData[];
|
||||
arguments: IArgument[];
|
||||
substitutions: ICstSubstituteEx[];
|
||||
}
|
||||
|
@ -150,6 +159,7 @@ export interface IOperationSchemaData extends ILibraryItemData {
|
|||
* Represents OperationSchema.
|
||||
*/
|
||||
export interface IOperationSchema extends IOperationSchemaData {
|
||||
items: IOperation[];
|
||||
graph: Graph;
|
||||
schemas: LibraryItemID[];
|
||||
stats: IOperationSchemaStats;
|
||||
|
@ -160,7 +170,7 @@ export interface IOperationSchema extends IOperationSchemaData {
|
|||
* Represents data response when creating {@link IOperation}.
|
||||
*/
|
||||
export interface IOperationCreatedResponse {
|
||||
new_operation: IOperation;
|
||||
new_operation: IOperationData;
|
||||
oss: IOperationSchemaData;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ export interface ITargetCst {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents Constituenta data from server.
|
||||
* Represents {@link IConstituenta} data from server.
|
||||
*/
|
||||
export interface IConstituentaData extends IConstituentaMeta {
|
||||
parse: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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 { urls } from '@/app/urls';
|
||||
|
@ -22,10 +22,11 @@ import TextInput from '@/components/ui/TextInput';
|
|||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { ILibraryCreateData } from '@/models/library';
|
||||
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';
|
||||
|
||||
function FormCreateItem() {
|
||||
|
@ -45,6 +46,7 @@ function FormCreateItem() {
|
|||
|
||||
const location = useMemo(() => combineLocation(head, body), [head, body]);
|
||||
const isValid = useMemo(() => validateLocation(location), [location]);
|
||||
const [initLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [file, setFile] = useState<File | undefined>();
|
||||
|
@ -104,6 +106,13 @@ function FormCreateItem() {
|
|||
setBody(newValue.length > 3 ? newValue.substring(3) : '');
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!initLocation) {
|
||||
return;
|
||||
}
|
||||
handleSelectLocation(initLocation);
|
||||
}, [initLocation, handleSelectLocation]);
|
||||
|
||||
return (
|
||||
<form className={clsx('cc-column', 'min-w-[30rem] max-w-[30rem] mx-auto', 'px-6 py-3')} onSubmit={handleSubmit}>
|
||||
<h1>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
IconMoveDown,
|
||||
IconMoveUp,
|
||||
IconNewItem,
|
||||
IconOSS,
|
||||
IconReset,
|
||||
IconSave,
|
||||
IconStatusOK,
|
||||
|
@ -22,6 +23,9 @@ function HelpCstEditor() {
|
|||
return (
|
||||
<div className='dense'>
|
||||
<h1>Редактор конституенты</h1>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
|
||||
</li>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
IconEditor,
|
||||
IconFollow,
|
||||
IconImmutable,
|
||||
IconOSS,
|
||||
IconOwner,
|
||||
IconPublic,
|
||||
IconSave
|
||||
|
@ -29,6 +30,9 @@ function HelpRSFormCard() {
|
|||
</p>
|
||||
|
||||
<h2>Управление</h2>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconSave className='inline-icon' /> сохранить изменения: Ctrl + S
|
||||
</li>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
IconMoveUp,
|
||||
IconNewItem,
|
||||
IconOpenList,
|
||||
IconOSS,
|
||||
IconReset
|
||||
} from '@/components/Icons';
|
||||
import InfoCstStatus from '@/components/info/InfoCstStatus';
|
||||
|
@ -27,6 +28,9 @@ function HelpRSFormItems() {
|
|||
</li>
|
||||
|
||||
<h2>Управление списком</h2>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconReset className='inline-icon' /> сбросить выделение: ESC
|
||||
</li>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
IconGraphOutputs,
|
||||
IconImage,
|
||||
IconNewItem,
|
||||
IconOSS,
|
||||
IconReset,
|
||||
IconRotate3D,
|
||||
IconText
|
||||
|
@ -70,6 +71,9 @@ function HelpTermGraph() {
|
|||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||
<div className='w-full sm:w-[14rem]'>
|
||||
<h1>Общие</h1>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
<li>
|
||||
<IconFilter className='inline-icon' /> Открыть настройки
|
||||
</li>
|
||||
|
|
|
@ -101,11 +101,11 @@ function NodeContextMenu({
|
|||
};
|
||||
|
||||
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}>
|
||||
<DropdownButton
|
||||
text='Редактировать'
|
||||
titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')}
|
||||
titleHtml={prepareTooltip('Редактировать операцию', 'Двойной клик')}
|
||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={handleEditOperation}
|
||||
|
|
|
@ -122,10 +122,43 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
controller.savePositions(getPositions(), () => setIsModified(false));
|
||||
}, [controller, getPositions, setIsModified]);
|
||||
|
||||
const handleCreateOperation = useCallback(() => {
|
||||
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
controller.promptCreateOperation(center.x, center.y, getPositions());
|
||||
}, [controller, getPositions, flow]);
|
||||
const handleCreateOperation = useCallback(
|
||||
(inputs: OperationID[]) => () => {
|
||||
if (!controller.schema) {
|
||||
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(() => {
|
||||
if (controller.selected.length !== 1) {
|
||||
|
@ -241,13 +274,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
handleContextMenuHide();
|
||||
}, [handleContextMenuHide]);
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
const handleNodeDoubleClick = useCallback(
|
||||
(event: CProps.EventMouse, node: OssNode) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleEditOperation(Number(node.id));
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleEditOperation(Number(node.id));
|
||||
},
|
||||
[handleEditOperation]
|
||||
);
|
||||
|
@ -268,7 +299,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
if ((event.ctrlKey || event.metaKey) && event.key === 'q') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleCreateOperation();
|
||||
handleCreateOperation(controller.selected);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
|
@ -297,7 +328,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
fitView
|
||||
nodeTypes={OssNodeTypes}
|
||||
|
@ -305,11 +336,11 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
minZoom={0.75}
|
||||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[10, 10]}
|
||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||
onNodeContextMenu={handleContextMenu}
|
||||
onClick={handleClickCanvas}
|
||||
>
|
||||
{showGrid ? <Background gap={10} /> : null}
|
||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
||||
</ReactFlow>
|
||||
),
|
||||
[
|
||||
|
@ -319,7 +350,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
handleContextMenu,
|
||||
handleClickCanvas,
|
||||
onEdgesChange,
|
||||
handleNodeClick,
|
||||
handleNodeDoubleClick,
|
||||
OssNodeTypes,
|
||||
showGrid
|
||||
]
|
||||
|
@ -334,7 +365,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
edgeAnimate={edgeAnimate}
|
||||
edgeStraight={edgeStraight}
|
||||
onFitView={handleFitView}
|
||||
onCreate={handleCreateOperation}
|
||||
onCreate={handleCreateOperation(controller.selected)}
|
||||
onDelete={handleDeleteSelected}
|
||||
onEdit={() => handleEditOperation(controller.selected[0])}
|
||||
onExecute={handleExecuteSelected}
|
||||
|
|
|
@ -167,7 +167,7 @@ function ToolbarOssGraph({
|
|||
onClick={onExecute}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Ctrl + клик')}
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||
onClick={onEdit}
|
||||
|
|
|
@ -51,7 +51,7 @@ export interface IOssEditContext {
|
|||
openOperationSchema: (target: OperationID) => 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;
|
||||
createInput: (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 [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
||||
const [initialInputs, setInitialInputs] = useState<OperationID[]>([]);
|
||||
const [positions, setPositions] = useState<IOperationPosition[]>([]);
|
||||
const [targetOperationID, setTargetOperationID] = useState<OperationID | undefined>(undefined);
|
||||
const targetOperation = useMemo(
|
||||
|
@ -208,11 +209,15 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
[model]
|
||||
);
|
||||
|
||||
const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => {
|
||||
setInsertPosition({ x: x, y: y });
|
||||
setPositions(positions);
|
||||
setShowCreateOperation(true);
|
||||
}, []);
|
||||
const promptCreateOperation = useCallback(
|
||||
(x: number, y: number, inputs: OperationID[], positions: IOperationPosition[]) => {
|
||||
setInsertPosition({ x: x, y: y });
|
||||
setInitialInputs(inputs);
|
||||
setPositions(positions);
|
||||
setShowCreateOperation(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreateOperation = useCallback(
|
||||
(data: IOperationCreateData) => {
|
||||
|
@ -341,6 +346,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
hideWindow={() => setShowCreateOperation(false)}
|
||||
oss={model.schema}
|
||||
onCreate={handleCreateOperation}
|
||||
initialInputs={initialInputs}
|
||||
/>
|
||||
) : null}
|
||||
{showEditInput ? (
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
IconSave
|
||||
} from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
@ -54,6 +55,12 @@ function ToolbarConstituenta({
|
|||
|
||||
return (
|
||||
<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
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
'use client';
|
||||
|
||||
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 Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
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 TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import useDropdown from '@/hooks/useDropdown';
|
||||
import { ILibraryUpdateData, LibraryItemType } from '@/models/library';
|
||||
import { limits, patterns, prefixes } from '@/utils/constants';
|
||||
import { limits, patterns } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import ToolbarItemAccess from './ToolbarItemAccess';
|
||||
|
@ -37,8 +32,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
const [visible, setVisible] = useState(false);
|
||||
const [readOnly, setReadOnly] = useState(false);
|
||||
|
||||
const ossMenu = useDropdown();
|
||||
|
||||
useEffect(() => {
|
||||
if (!schema) {
|
||||
setIsModified(false);
|
||||
|
@ -92,35 +85,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
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 (
|
||||
<form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}>
|
||||
<TextInput
|
||||
|
@ -132,7 +96,6 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
|||
disabled={!controller.isContentEditable}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
/>
|
||||
{ossSelector}
|
||||
<div className='flex justify-between w-full gap-3 mb-3'>
|
||||
<TextInput
|
||||
id='schema_alias'
|
||||
|
|
|
@ -29,7 +29,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
|
|||
);
|
||||
|
||||
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' />
|
||||
<div className='ml-auto cc-icons'>
|
||||
<SelectAccessPolicy
|
||||
|
|
|
@ -5,15 +5,19 @@ import { useMemo } from 'react';
|
|||
import { SubscribeIcon } from '@/components/DomainIcons';
|
||||
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { AccessPolicy, ILibraryItemEditor } from '@/models/library';
|
||||
import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/library';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { UserLevel } from '@/models/user';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||
|
||||
import { IRSEditContext } from '../RSEditContext';
|
||||
|
||||
interface ToolbarRSFormCardProps {
|
||||
modified: boolean;
|
||||
subscribed: boolean;
|
||||
|
@ -33,8 +37,26 @@ function ToolbarRSFormCard({
|
|||
}: ToolbarRSFormCardProps) {
|
||||
const { accessLevel } = useAccessMode();
|
||||
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 (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='cc-icons'>
|
||||
{ossSelector}
|
||||
{controller.isMutable || modified ? (
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface ToolbarVersioningProps {
|
|||
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
||||
const controller = useRSEdit();
|
||||
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 ? (
|
||||
<>
|
||||
<MiniButton
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
IconReset
|
||||
} from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
import DropdownButton from '@/components/ui/DropdownButton';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -27,6 +28,12 @@ function ToolbarRSList() {
|
|||
|
||||
return (
|
||||
<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
|
||||
titleHtml={prepareTooltip('Сбросить выделение', 'ESC')}
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
IconTextOff
|
||||
} from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -55,6 +56,12 @@ function ToolbarTermGraph({
|
|||
|
||||
return (
|
||||
<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
|
||||
title='Настройки фильтрации узлов и связей'
|
||||
icon={<IconFilter size='1.25rem' className='icon-primary' />}
|
||||
|
|
|
@ -163,9 +163,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|||
/>
|
||||
{controller.isContentEditable ? (
|
||||
<DropdownButton
|
||||
text='Загрузить из Экстеора'
|
||||
text='Загрузить из Экстеор'
|
||||
icon={<IconUpload size='1rem' className='icon-red' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={controller.isProcessing || controller.schema?.oss.length !== 0}
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -15,6 +15,8 @@ export const PARAMETER = {
|
|||
ossImageWidth: 1280, // pixels - size of OSS image
|
||||
ossImageHeight: 960, // pixels - size of OSS image
|
||||
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
|
||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||
|
|
Loading…
Reference in New Issue
Block a user