F: Improve RSForm and OSS UI + some fixes

This commit is contained in:
Ivan 2024-08-01 20:10:58 +03:00
parent 62cb9a5eeb
commit e29f7409c1
27 changed files with 303 additions and 93 deletions

View File

@ -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()

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 { 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>
);
}

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 { 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,

View File

@ -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

View File

@ -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),

View File

@ -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 {

View File

@ -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;
}

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 {
parse: {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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));
}
},
[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}

View File

@ -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}

View File

@ -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[]) => {
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 ? (

View File

@ -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' />}

View File

@ -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'

View File

@ -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

View File

@ -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')}

View File

@ -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

View File

@ -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' />}

View File

@ -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' />}

View File

@ -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}

View File

@ -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