F: Add react-scan to tooling and fix some rerenders

This commit is contained in:
Ivan 2025-02-20 14:45:12 +03:00
parent 45dbe16444
commit 12ddc007ac
36 changed files with 1207 additions and 518 deletions

View File

@ -40,6 +40,7 @@ This readme file is used mostly to document project dependencies and conventions
- react-tooltip - react-tooltip
- react-zoom-pan-pinch - react-zoom-pan-pinch
- react-hook-form - react-hook-form
- react-scan
- reactflow - reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce

View File

@ -12,6 +12,8 @@
<meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" /> <meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" />
<meta name="yandex-verification" content="2b1f1f721cd6b66a" /> <meta name="yandex-verification" content="2b1f1f721cd6b66a" />
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-intl": "^7.1.6", "react-intl": "^7.1.6",
"react-router": "^7.2.0", "react-router": "^7.2.0",
"react-scan": "^0.1.3",
"react-select": "^5.10.0", "react-select": "^5.10.0",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
"react-toastify": "^11.0.3", "react-toastify": "^11.0.3",

View File

@ -3,7 +3,6 @@ import { Outlet } from 'react-router';
import { ModalLoader } from '@/components/Modal'; import { ModalLoader } from '@/components/Modal';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants';
import { NavigationState } from './Navigation/NavigationContext'; import { NavigationState } from './Navigation/NavigationContext';
import { Footer } from './Footer'; import { Footer } from './Footer';
@ -40,13 +39,7 @@ export function ApplicationLayout() {
<Navigation /> <Navigation />
<div <div className='overflow-x-auto max-w-[100vw]' style={{ maxHeight: viewportHeight }}>
id={globals.main_scroll}
className='overflow-x-auto max-w-[100vw]'
style={{
maxHeight: viewportHeight
}}
>
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}> <main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<GlobalLoader /> <GlobalLoader />
<MutationErrors /> <MutationErrors />

View File

@ -8,7 +8,7 @@ export function ErrorFallback() {
const router = useNavigate(); const router = useNavigate();
function resetErrorBoundary() { function resetErrorBoundary() {
Promise.resolve(router('/')).catch(console.log); Promise.resolve(router('/')).catch(console.error);
} }
return ( return (
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'> <div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>

View File

@ -1,14 +1,23 @@
'use client'; 'use client';
import { InfoConstituenta } from '@/features/rsform/components/InfoConstituenta'; import React, { Suspense } from 'react';
import { Tooltip } from '@/components/Container'; import { Tooltip } from '@/components/Container';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { useTooltipsStore } from '@/stores/tooltips'; import { useTooltipsStore } from '@/stores/tooltips';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
const InfoConstituenta = React.lazy(() =>
import('@/features/rsform/components/InfoConstituenta').then(module => ({ default: module.InfoConstituenta }))
);
const InfoOperation = React.lazy(() =>
import('@/features/oss/components/InfoOperation').then(module => ({ default: module.InfoOperation }))
);
export const GlobalTooltips = () => { export const GlobalTooltips = () => {
const hoverCst = useTooltipsStore(state => state.activeCst); const hoverCst = useTooltipsStore(state => state.activeCst);
const hoverOperation = useTooltipsStore(state => state.activeOperation);
return ( return (
<> <>
@ -25,8 +34,26 @@ export const GlobalTooltips = () => {
layer='z-topmost' layer='z-topmost'
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify' className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
/> />
<Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'> <Tooltip
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />} clickable
id={globals.constituenta_tooltip}
layer='z-modalTooltip'
className='max-w-[30rem]'
hidden={!hoverCst}
>
<Suspense fallback={<Loader />}>
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : null}
</Suspense>
</Tooltip>
<Tooltip
id={globals.operation_tooltip}
layer='z-modalTooltip'
className='max-w-[35rem] max-h-[40rem] dense'
hidden={!hoverOperation}
>
<Suspense fallback={<Loader />}>
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
</Suspense>
</Tooltip> </Tooltip>
</> </>
); );

View File

@ -1,9 +1,8 @@
'use client'; 'use client';
import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { globals } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
interface INavigationContext { interface INavigationContext {
@ -29,68 +28,48 @@ export const useConceptNavigation = () => {
export const NavigationState = ({ children }: React.PropsWithChildren) => { export const NavigationState = ({ children }: React.PropsWithChildren) => {
const router = useNavigate(); const router = useNavigate();
const { pathname } = useLocation();
const [isBlocked, setIsBlocked] = useState(false); const [isBlocked, setIsBlocked] = useState(false);
const validate = useCallback(() => {
function validate() {
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?'); return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
}, [isBlocked]); }
const canBack = useCallback(() => !!window.history && window.history?.length !== 0, []); function canBack() {
return !!window.history && window.history?.length !== 0;
}
const scrollTop = useCallback(() => { function push(path: string, newTab?: boolean) {
window.scrollTo(0, 0); if (newTab) {
const mainScroll = document.getElementById(globals.main_scroll); window.open(`${path}`, '_blank');
if (mainScroll) { return;
mainScroll.scroll(0, 0);
} }
}, []);
const push = useCallback(
(path: string, newTab?: boolean) => {
if (newTab) {
window.open(`${path}`, '_blank');
return;
}
if (validate()) {
scrollTop();
Promise.resolve(router(path, { viewTransition: true })).catch(console.log);
setIsBlocked(false);
}
},
[router, validate, scrollTop]
);
const replace = useCallback(
(path: string) => {
if (validate()) {
scrollTop();
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.log);
setIsBlocked(false);
}
},
[router, validate, scrollTop]
);
const back = useCallback(() => {
if (validate()) { if (validate()) {
scrollTop(); Promise.resolve(router(path, { viewTransition: true })).catch(console.error);
Promise.resolve(router(-1)).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
}, [router, validate, scrollTop]); }
const forward = useCallback(() => { function replace(path: string) {
if (validate()) { if (validate()) {
scrollTop(); Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.error);
Promise.resolve(router(1)).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
}, [router, validate, scrollTop]); }
useEffect(() => { function back() {
scrollTop(); if (validate()) {
}, [pathname, scrollTop]); Promise.resolve(router(-1)).catch(console.error);
setIsBlocked(false);
}
}
function forward() {
if (validate()) {
Promise.resolve(router(1)).catch(console.error);
setIsBlocked(false);
}
}
return ( return (
<NavigationContext <NavigationContext

View File

@ -1,18 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useMutationState, useQueryClient } from '@tanstack/react-query'; import { useMutationState } from '@tanstack/react-query';
import { KEYS } from './configuration'; import { KEYS } from './configuration';
export const useMutationErrors = () => { export const useMutationErrors = () => {
const queryClient = useQueryClient();
const [ignored, setIgnored] = useState<Error[]>([]); const [ignored, setIgnored] = useState<Error[]>([]);
const mutationErrors = useMutationState({ const mutationErrors = useMutationState({
filters: { mutationKey: [KEYS.global_mutation], status: 'error' }, filters: { mutationKey: [KEYS.global_mutation], status: 'error' },
select: mutation => mutation.state.error! select: mutation => mutation.state.error!
}); });
console.log(queryClient.getMutationCache().getAll());
function resetErrors() { function resetErrors() {
setIgnored(mutationErrors); setIgnored(mutationErrors);
} }

View File

@ -29,22 +29,12 @@ import { useSetLocation } from '../backend/useSetLocation';
import { useSetOwner } from '../backend/useSetOwner'; import { useSetOwner } from '../backend/useSetOwner';
import { useLibrarySearchStore } from '../stores/librarySearch'; import { useLibrarySearchStore } from '../stores/librarySearch';
/** interface EditorLibraryItemProps {
* Represents common {@link ILibraryItem} editor controller.
*/
export interface ILibraryItemEditor {
schema: ILibraryItemData; schema: ILibraryItemData;
deleteSchema: () => void;
isMutable: boolean;
isAttachedToOSS: boolean; isAttachedToOSS: boolean;
} }
interface EditorLibraryItemProps { export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItemProps) {
controller: ILibraryItemEditor;
}
export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
const getUserLabel = useLabelUser(); const getUserLabel = useLabelUser();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const intl = useIntl(); const intl = useIntl();
@ -63,31 +53,31 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
const ownerSelector = useDropdown(); const ownerSelector = useDropdown();
const onSelectUser = function (newValue: number) { const onSelectUser = function (newValue: number) {
ownerSelector.hide(); ownerSelector.hide();
if (newValue === controller.schema.owner) { if (newValue === schema.owner) {
return; return;
} }
if (!window.confirm(promptText.ownerChange)) { if (!window.confirm(promptText.ownerChange)) {
return; return;
} }
void setOwner({ itemID: controller.schema.id, owner: newValue }); void setOwner({ itemID: schema.id, owner: newValue });
}; };
function handleOpenLibrary(event: CProps.EventMouse) { function handleOpenLibrary(event: CProps.EventMouse) {
setGlobalLocation(controller.schema.location); setGlobalLocation(schema.location);
router.push(urls.library, event.ctrlKey || event.metaKey); router.push(urls.library, event.ctrlKey || event.metaKey);
} }
function handleEditLocation() { function handleEditLocation() {
showEditLocation({ showEditLocation({
initial: controller.schema.location, initial: schema.location,
onChangeLocation: newLocation => void setLocation({ itemID: controller.schema.id, location: newLocation }) onChangeLocation: newLocation => void setLocation({ itemID: schema.id, location: newLocation })
}); });
} }
function handleEditEditors() { function handleEditEditors() {
showEditEditors({ showEditEditors({
itemID: controller.schema.id, itemID: schema.id,
initialEditors: controller.schema.editors initialEditors: schema.editors
}); });
} }
@ -104,31 +94,27 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
<ValueIcon <ValueIcon
className='text-ellipsis flex-grow' className='text-ellipsis flex-grow'
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />} icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
value={controller.schema.location} value={schema.location}
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'} title={isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
onClick={handleEditLocation} onClick={handleEditLocation}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER} disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
/> />
</div> </div>
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<Overlay position='top-[-0.5rem] left-[4rem] cc-icons'> <Overlay position='top-[-0.5rem] left-[4rem] cc-icons'>
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<SelectUser <SelectUser className='w-[25rem] sm:w-[26rem] text-sm' value={schema.owner} onChange={onSelectUser} />
className='w-[25rem] sm:w-[26rem] text-sm'
value={controller.schema.owner}
onChange={onSelectUser}
/>
) : null} ) : null}
</Overlay> </Overlay>
) : null} ) : null}
<ValueIcon <ValueIcon
className='sm:mb-1' className='sm:mb-1'
icon={<IconOwner size='1.25rem' className='icon-primary' />} icon={<IconOwner size='1.25rem' className='icon-primary' />}
value={getUserLabel(controller.schema.owner)} value={getUserLabel(schema.owner)}
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'} title={isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
onClick={ownerSelector.toggle} onClick={ownerSelector.toggle}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER} disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
/> />
<div className='sm:mb-1 flex justify-between items-center'> <div className='sm:mb-1 flex justify-between items-center'>
@ -136,13 +122,13 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
id='editor_stats' id='editor_stats'
dense dense
icon={<IconEditor size='1.25rem' className='icon-primary' />} icon={<IconEditor size='1.25rem' className='icon-primary' />}
value={controller.schema.editors.length} value={schema.editors.length}
onClick={handleEditEditors} onClick={handleEditEditors}
disabled={isModified || isProcessing || role < UserRole.OWNER} disabled={isModified || isProcessing || role < UserRole.OWNER}
/> />
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'> <Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
<Suspense fallback={<Loader scale={2} />}> <Suspense fallback={<Loader scale={2} />}>
<InfoUsers items={controller.schema.editors} prefix={prefixes.user_editors} header='Редакторы' /> <InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
</Suspense> </Suspense>
</Tooltip> </Tooltip>
@ -150,7 +136,7 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
dense dense
disabled disabled
icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />} icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />}
value={new Date(controller.schema.time_update).toLocaleString(intl.locale)} value={new Date(schema.time_update).toLocaleString(intl.locale)}
title='Дата обновления' title='Дата обновления'
/> />
@ -158,7 +144,7 @@ export function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
dense dense
disabled disabled
icon={<IconDateCreate size='1.25rem' className='text-ok-600' />} icon={<IconDateCreate size='1.25rem' className='text-ok-600' />}
value={new Date(controller.schema.time_create).toLocaleString(intl.locale, { value={new Date(schema.time_create).toLocaleString(intl.locale, {
year: '2-digit', year: '2-digit',
month: '2-digit', month: '2-digit',
day: '2-digit' day: '2-digit'

View File

@ -8,11 +8,10 @@ import { IconImmutable, IconMutable } from '@/components/Icons';
import { Label } from '@/components/Input'; import { Label } from '@/components/Input';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { AccessPolicy } from '../backend/types'; import { AccessPolicy, ILibraryItem } from '../backend/types';
import { useMutatingLibrary } from '../backend/useMutatingLibrary'; import { useMutatingLibrary } from '../backend/useMutatingLibrary';
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy'; import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
import { ILibraryItemEditor } from './EditorLibraryItem';
import { SelectAccessPolicy } from './SelectAccessPolicy'; import { SelectAccessPolicy } from './SelectAccessPolicy';
interface ToolbarItemAccessProps { interface ToolbarItemAccessProps {
@ -20,7 +19,8 @@ interface ToolbarItemAccessProps {
toggleVisible: () => void; toggleVisible: () => void;
readOnly: boolean; readOnly: boolean;
toggleReadOnly: () => void; toggleReadOnly: () => void;
controller: ILibraryItemEditor; schema: ILibraryItem;
isAttachedToOSS: boolean;
} }
export function ToolbarItemAccess({ export function ToolbarItemAccess({
@ -28,15 +28,16 @@ export function ToolbarItemAccess({
toggleVisible, toggleVisible,
readOnly, readOnly,
toggleReadOnly, toggleReadOnly,
controller schema,
isAttachedToOSS
}: ToolbarItemAccessProps) { }: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const isProcessing = useMutatingLibrary(); const isProcessing = useMutatingLibrary();
const policy = controller.schema.access_policy; const policy = schema.access_policy;
const { setAccessPolicy } = useSetAccessPolicy(); const { setAccessPolicy } = useSetAccessPolicy();
function handleSetAccessPolicy(newPolicy: AccessPolicy) { function handleSetAccessPolicy(newPolicy: AccessPolicy) {
void setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy }); void setAccessPolicy({ itemID: schema.id, policy: newPolicy });
} }
return ( return (
@ -44,7 +45,7 @@ export function ToolbarItemAccess({
<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
disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS} disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
value={policy} value={policy}
onChange={handleSetAccessPolicy} onChange={handleSetAccessPolicy}
/> />

View File

@ -6,7 +6,7 @@ export { useTemplatesSuspense } from './backend/useTemplates';
export { useUpdateItem } from './backend/useUpdateItem'; export { useUpdateItem } from './backend/useUpdateItem';
export { useUpdateTimestamp } from './backend/useUpdateTimestamp'; export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
export { useVersionRestore } from './backend/useVersionRestore'; export { useVersionRestore } from './backend/useVersionRestore';
export { EditorLibraryItem, type ILibraryItemEditor } from './components/EditorLibraryItem'; export { EditorLibraryItem } from './components/EditorLibraryItem';
export { MiniSelectorOSS } from './components/MiniSelectorOSS'; export { MiniSelectorOSS } from './components/MiniSelectorOSS';
export { PickSchema } from './components/PickSchema'; export { PickSchema } from './components/PickSchema';
export { SelectLibraryItem } from './components/SelectLibraryItem'; export { SelectLibraryItem } from './components/SelectLibraryItem';

View File

@ -2,22 +2,20 @@
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import { Tooltip } from '@/components/Container';
import { DataTable } from '@/components/DataTable'; import { DataTable } from '@/components/DataTable';
import { IconPageRight } from '@/components/Icons'; import { IconPageRight } from '@/components/Icons';
import { ICstSubstituteInfo, OperationType } from '../backend/types'; import { ICstSubstituteInfo, OperationType } from '../backend/types';
import { labelOperationType } from '../labels'; import { labelOperationType } from '../labels';
import { OssNodeInternal } from '../models/ossLayout'; import { IOperation } from '../models/oss';
interface TooltipOperationProps { interface InfoOperationProps {
node: OssNodeInternal; operation: IOperation;
anchor: string;
} }
const columnHelper = createColumnHelper<ICstSubstituteInfo>(); const columnHelper = createColumnHelper<ICstSubstituteInfo>();
export function TooltipOperation({ node, anchor }: TooltipOperationProps) { export function InfoOperation({ operation }: InfoOperationProps) {
const columns = [ const columns = [
columnHelper.accessor('substitution_term', { columnHelper.accessor('substitution_term', {
id: 'substitution_term', id: 'substitution_term',
@ -44,47 +42,47 @@ export function TooltipOperation({ node, anchor }: TooltipOperationProps) {
]; ];
return ( return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[35rem] max-h-[40rem] dense'> <>
<h2>{node.data.operation.alias}</h2> <h2>{operation.alias}</h2>
<p> <p>
<b>Тип:</b> {labelOperationType(node.data.operation.operation_type)} <b>Тип:</b> {labelOperationType(operation.operation_type)}
</p> </p>
{!node.data.operation.is_owned ? ( {!operation.is_owned ? (
<p> <p>
<b>КС не принадлежит ОСС</b> <b>КС не принадлежит ОСС</b>
</p> </p>
) : null} ) : null}
{node.data.operation.is_consolidation ? ( {operation.is_consolidation ? (
<p> <p>
<b>Ромбовидный синтез</b> <b>Ромбовидный синтез</b>
</p> </p>
) : null} ) : null}
{node.data.operation.title ? ( {operation.title ? (
<p> <p>
<b>Название: </b> <b>Название: </b>
{node.data.operation.title} {operation.title}
</p> </p>
) : null} ) : null}
{node.data.operation.comment ? ( {operation.comment ? (
<p> <p>
<b>Комментарий: </b> <b>Комментарий: </b>
{node.data.operation.comment} {operation.comment}
</p> </p>
) : null} ) : null}
{node.data.operation.substitutions.length > 0 ? ( {operation.substitutions.length > 0 ? (
<DataTable <DataTable
dense dense
noHeader noHeader
noFooter noFooter
className='text-sm border select-none mb-2' className='text-sm border select-none mb-2'
data={node.data.operation.substitutions} data={operation.substitutions}
columns={columns} columns={columns}
/> />
) : node.data.operation.operation_type !== OperationType.INPUT ? ( ) : operation.operation_type !== OperationType.INPUT ? (
<p> <p>
<b>Отождествления:</b> Отсутствуют <b>Отождествления:</b> Отсутствуют
</p> </p>
) : null} ) : null}
</Tooltip> </>
); );
} }

View File

@ -15,7 +15,7 @@ import { FormOSS } from './FormOSS';
import { OssStats } from './OssStats'; import { OssStats } from './OssStats';
export function EditorOssCard() { export function EditorOssCard() {
const controller = useOssEdit(); const { schema, isMutable, deleteSchema } = useOssEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
function initiateSubmit() { function initiateSubmit() {
@ -36,7 +36,7 @@ export function EditorOssCard() {
return ( return (
<> <>
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} /> <ToolbarRSFormCard onSubmit={initiateSubmit} schema={schema} isMutable={isMutable} deleteSchema={deleteSchema} />
<div <div
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx(
@ -48,10 +48,10 @@ export function EditorOssCard() {
> >
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS /> <FormOSS />
<EditorLibraryItem controller={controller} /> <EditorLibraryItem schema={schema} isAttachedToOSS={false} />
</FlexColumn> </FlexColumn>
<OssStats stats={controller.schema.stats} /> <OssStats stats={schema.stats} />
</div> </div>
</> </>
); );

View File

@ -20,9 +20,9 @@ import { useOssEdit } from '../OssEditContext';
export function FormOSS() { export function FormOSS() {
const { updateItem: updateOss } = useUpdateItem(); const { updateItem: updateOss } = useUpdateItem();
const controller = useOssEdit();
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { schema, isMutable } = useOssEdit();
const { const {
register, register,
@ -34,13 +34,13 @@ export function FormOSS() {
} = useForm<IUpdateLibraryItemDTO>({ } = useForm<IUpdateLibraryItemDTO>({
resolver: zodResolver(schemaUpdateLibraryItem), resolver: zodResolver(schemaUpdateLibraryItem),
defaultValues: { defaultValues: {
id: controller.schema.id, id: schema.id,
item_type: LibraryItemType.RSFORM, item_type: LibraryItemType.RSFORM,
title: controller.schema.title, title: schema.title,
alias: controller.schema.alias, alias: schema.alias,
comment: controller.schema.comment, comment: schema.comment,
visible: controller.schema.visible, visible: schema.visible,
read_only: controller.schema.read_only read_only: schema.read_only
} }
}); });
const visible = useWatch({ control, name: 'visible' }); const visible = useWatch({ control, name: 'visible' });
@ -65,7 +65,7 @@ export function FormOSS() {
{...register('title')} {...register('title')}
label='Полное название' label='Полное название'
className='mb-3' className='mb-3'
disabled={!controller.isMutable} disabled={!isMutable}
error={errors.title} error={errors.title}
/> />
<div className='flex justify-between gap-3 mb-3'> <div className='flex justify-between gap-3 mb-3'>
@ -74,7 +74,7 @@ export function FormOSS() {
{...register('alias')} {...register('alias')}
label='Сокращение' label='Сокращение'
className='w-[16rem]' className='w-[16rem]'
disabled={!controller.isMutable} disabled={!isMutable}
error={errors.alias} error={errors.alias}
/> />
<ToolbarItemAccess <ToolbarItemAccess
@ -82,7 +82,8 @@ export function FormOSS() {
toggleVisible={() => setValue('visible', !visible, { shouldDirty: true })} toggleVisible={() => setValue('visible', !visible, { shouldDirty: true })}
readOnly={readOnly} readOnly={readOnly}
toggleReadOnly={() => setValue('read_only', !readOnly, { shouldDirty: true })} toggleReadOnly={() => setValue('read_only', !readOnly, { shouldDirty: true })}
controller={controller} schema={schema}
isAttachedToOSS={false}
/> />
</div> </div>
@ -91,10 +92,10 @@ export function FormOSS() {
{...register('comment')} {...register('comment')}
label='Описание' label='Описание'
rows={3} rows={3}
disabled={!controller.isMutable || isProcessing} disabled={!isMutable || isProcessing}
error={errors.comment} error={errors.comment}
/> />
{controller.isMutable || isModified ? ( {isMutable || isModified ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center mt-4' className='self-center mt-4'

View File

@ -22,7 +22,7 @@ import { IOperation } from '../../../models/oss';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
export interface ContextMenuData { export interface ContextMenuData {
operation?: IOperation; operation: IOperation;
cursorX: number; cursorX: number;
cursorY: number; cursorY: number;
} }
@ -51,24 +51,24 @@ export function NodeContextMenu({
onExecuteOperation, onExecuteOperation,
onRelocateConstituents onRelocateConstituents
}: NodeContextMenuProps) { }: NodeContextMenuProps) {
const controller = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { schema, navigateOperationSchema, isMutable, canDelete } = useOssEdit();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const readyForSynthesis = (() => { const readyForSynthesis = (() => {
if (operation?.operation_type !== OperationType.SYNTHESIS) { if (operation.operation_type !== OperationType.SYNTHESIS) {
return false; return false;
} }
if (operation.result) { if (operation.result) {
return false; return false;
} }
const argumentIDs = controller.schema.graph.expandInputs([operation.id]); const argumentIDs = schema.graph.expandInputs([operation.id]);
if (!argumentIDs || argumentIDs.length < 1) { if (!argumentIDs || argumentIDs.length < 1) {
return false; return false;
} }
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!); const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) { if (argumentOperations.some(item => item.result === null)) {
return false; return false;
} }
@ -79,37 +79,37 @@ export function NodeContextMenu({
useClickedOutside(isOpen, ref, onHide); useClickedOutside(isOpen, ref, onHide);
function handleOpenSchema() { function handleOpenSchema() {
if (operation) controller.navigateOperationSchema(operation.id); navigateOperationSchema(operation.id);
} }
function handleEditSchema() { function handleEditSchema() {
onHide(); onHide();
if (operation) onEditSchema(operation.id); onEditSchema(operation.id);
} }
function handleEditOperation() { function handleEditOperation() {
onHide(); onHide();
if (operation) onEditOperation(operation.id); onEditOperation(operation.id);
} }
function handleDeleteOperation() { function handleDeleteOperation() {
onHide(); onHide();
if (operation) onDelete(operation.id); onDelete(operation.id);
} }
function handleCreateSchema() { function handleCreateSchema() {
onHide(); onHide();
if (operation) onCreateInput(operation.id); onCreateInput(operation.id);
} }
function handleRunSynthesis() { function handleRunSynthesis() {
onHide(); onHide();
if (operation) onExecuteOperation(operation.id); onExecuteOperation(operation.id);
} }
function handleRelocateConstituents() { function handleRelocateConstituents() {
onHide(); onHide();
if (operation) onRelocateConstituents(operation.id); onRelocateConstituents(operation.id);
} }
return ( return (
@ -123,7 +123,7 @@ export function NodeContextMenu({
text='Редактировать' text='Редактировать'
title='Редактировать операцию' title='Редактировать операцию'
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={!controller.isMutable || isProcessing} disabled={!isMutable || isProcessing}
onClick={handleEditOperation} onClick={handleEditOperation}
/> />
@ -136,7 +136,7 @@ export function NodeContextMenu({
onClick={handleOpenSchema} onClick={handleOpenSchema}
/> />
) : null} ) : null}
{controller.isMutable && !operation?.result && operation?.operation_type === OperationType.INPUT ? ( {isMutable && !operation?.result && operation?.operation_type === OperationType.INPUT ? (
<DropdownButton <DropdownButton
text='Создать схему' text='Создать схему'
title='Создать пустую схему для загрузки' title='Создать пустую схему для загрузки'
@ -145,7 +145,7 @@ export function NodeContextMenu({
onClick={handleCreateSchema} onClick={handleCreateSchema}
/> />
) : null} ) : null}
{controller.isMutable && operation?.operation_type === OperationType.INPUT ? ( {isMutable && operation?.operation_type === OperationType.INPUT ? (
<DropdownButton <DropdownButton
text={!operation?.result ? 'Загрузить схему' : 'Изменить схему'} text={!operation?.result ? 'Загрузить схему' : 'Изменить схему'}
title='Выбрать схему для загрузки' title='Выбрать схему для загрузки'
@ -154,7 +154,7 @@ export function NodeContextMenu({
onClick={handleEditSchema} onClick={handleEditSchema}
/> />
) : null} ) : null}
{controller.isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? ( {isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton <DropdownButton
text='Активировать синтез' text='Активировать синтез'
titleHtml={ titleHtml={
@ -168,7 +168,7 @@ export function NodeContextMenu({
/> />
) : null} ) : null}
{controller.isMutable && operation?.result ? ( {isMutable && operation?.result ? (
<DropdownButton <DropdownButton
text='Конституенты' text='Конституенты'
titleHtml='Перенос конституент</br>между схемами' titleHtml='Перенос конституент</br>между схемами'
@ -181,7 +181,7 @@ export function NodeContextMenu({
<DropdownButton <DropdownButton
text='Удалить операцию' text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!controller.isMutable || isProcessing || !operation || !controller.canDelete(operation.id)} disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)}
onClick={handleDeleteOperation} onClick={handleDeleteOperation}
/> />
</Dropdown> </Dropdown>

View File

@ -23,6 +23,7 @@ import { Overlay } from '@/components/Container';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { useTooltipsStore } from '@/stores/tooltips';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
@ -44,7 +45,19 @@ const ZOOM_MIN = 0.5;
export function OssFlow() { export function OssFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const controller = useOssEdit(); const {
navigateOperationSchema,
schema,
setSelected,
selected,
isMutable,
promptCreateOperation,
canDelete,
promptDeleteOperation,
promptEditInput,
promptEditOperation,
promptRelocateConstituents
} = useOssEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const flow = useReactFlow(); const flow = useReactFlow();
@ -52,6 +65,8 @@ export function OssFlow() {
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const setHoverOperation = useTooltipsStore(state => state.setActiveOperation);
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
@ -63,12 +78,12 @@ export function OssFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: undefined, cursorX: 0, cursorY: 0 }); const [menuProps, setMenuProps] = useState<ContextMenuData | null>(null);
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => Number(node.id));
controller.setSelected(prev => [ setSelected(prev => [
...prev.filter(nodeID => ids.includes(nodeID)), ...prev.filter(nodeID => ids.includes(nodeID)),
...ids.filter(nodeID => !prev.includes(Number(nodeID))) ...ids.filter(nodeID => !prev.includes(Number(nodeID)))
]); ]);
@ -80,7 +95,7 @@ export function OssFlow() {
useEffect(() => { useEffect(() => {
setNodes( setNodes(
controller.schema.items.map(operation => ({ schema.items.map(operation => ({
id: String(operation.id), id: String(operation.id),
data: { label: operation.alias, operation: operation }, data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y }, position: { x: operation.position_x, y: operation.position_y },
@ -88,15 +103,15 @@ export function OssFlow() {
})) }))
); );
setEdges( setEdges(
controller.schema.arguments.map((argument, index) => ({ schema.arguments.map((argument, index) => ({
id: String(index), id: String(index),
source: String(argument.argument), source: String(argument.argument),
target: String(argument.operation), target: String(argument.operation),
type: edgeStraight ? 'straight' : 'simplebezier', type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate, animated: edgeAnimate,
targetHandle: targetHandle:
controller.schema.operationByID.get(argument.argument)!.position_x > schema.operationByID.get(argument.argument)!.position_x >
controller.schema.operationByID.get(argument.operation)!.position_x schema.operationByID.get(argument.operation)!.position_x
? 'right' ? 'right'
: 'left' : 'left'
})) }))
@ -105,7 +120,7 @@ export function OssFlow() {
setTimeout(() => { setTimeout(() => {
setIsModified(false); setIsModified(false);
}, PARAMETER.graphRefreshDelay); }, PARAMETER.graphRefreshDelay);
}, [controller.schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]); }, [schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]);
function getPositions() { function getPositions() {
return nodes.map(node => ({ return nodes.map(node => ({
@ -116,7 +131,7 @@ export function OssFlow() {
} }
function handleNodesChange(changes: NodeChange[]) { function handleNodesChange(changes: NodeChange[]) {
if (controller.isMutable && changes.some(change => change.type === 'position' && change.position)) { if (isMutable && changes.some(change => change.type === 'position' && change.position)) {
setIsModified(true); setIsModified(true);
} }
onNodesChange(changes); onNodesChange(changes);
@ -124,9 +139,9 @@ export function OssFlow() {
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); const positions = getPositions();
void updatePositions({ itemID: controller.schema.id, positions: positions }).then(() => { void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => { positions.forEach(item => {
const operation = controller.schema.operationByID.get(item.id); const operation = schema.operationByID.get(item.id);
if (operation) { if (operation) {
operation.position_x = item.position_x; operation.position_x = item.position_x;
operation.position_y = item.position_y; operation.position_y = item.position_y;
@ -139,7 +154,7 @@ export function OssFlow() {
function handleCreateOperation(inputs: number[]) { function handleCreateOperation(inputs: number[]) {
const positions = getPositions(); const positions = getPositions();
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
controller.promptCreateOperation({ promptCreateOperation({
defaultX: target.x, defaultX: target.x,
defaultY: target.y, defaultY: target.y,
inputs: inputs, inputs: inputs,
@ -149,58 +164,58 @@ export function OssFlow() {
} }
function handleDeleteOperation(target: number) { function handleDeleteOperation(target: number) {
if (!controller.canDelete(target)) { if (!canDelete(target)) {
return; return;
} }
controller.promptDeleteOperation(target, getPositions()); promptDeleteOperation(target, getPositions());
} }
function handleDeleteSelected() { function handleDeleteSelected() {
if (controller.selected.length !== 1) { if (selected.length !== 1) {
return; return;
} }
handleDeleteOperation(controller.selected[0]); handleDeleteOperation(selected[0]);
} }
function handleInputCreate(target: number) { function handleInputCreate(target: number) {
const operation = controller.schema.operationByID.get(target); const operation = schema.operationByID.get(target);
if (!operation) { if (!operation) {
return; return;
} }
if (libraryItems.find(item => item.alias === operation.alias && item.location === controller.schema.location)) { if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
toast.error(errorMsg.inputAlreadyExists); toast.error(errorMsg.inputAlreadyExists);
return; return;
} }
void inputCreate({ void inputCreate({
itemID: controller.schema.id, itemID: schema.id,
data: { target: target, positions: getPositions() } data: { target: target, positions: getPositions() }
}).then(new_schema => router.push(urls.schema(new_schema.id))); }).then(new_schema => router.push(urls.schema(new_schema.id)));
} }
function handleEditSchema(target: number) { function handleEditSchema(target: number) {
controller.promptEditInput(target, getPositions()); promptEditInput(target, getPositions());
} }
function handleEditOperation(target: number) { function handleEditOperation(target: number) {
controller.promptEditOperation(target, getPositions()); promptEditOperation(target, getPositions());
} }
function handleOperationExecute(target: number) { function handleOperationExecute(target: number) {
void operationExecute({ void operationExecute({
itemID: controller.schema.id, // itemID: schema.id, //
data: { target: target, positions: getPositions() } data: { target: target, positions: getPositions() }
}); });
} }
function handleExecuteSelected() { function handleExecuteSelected() {
if (controller.selected.length !== 1) { if (selected.length !== 1) {
return; return;
} }
handleOperationExecute(controller.selected[0]); handleOperationExecute(selected[0]);
} }
function handleRelocateConstituents(target: number) { function handleRelocateConstituents(target: number) {
controller.promptRelocateConstituents(target, getPositions()); promptRelocateConstituents(target, getPositions());
} }
function handleSaveImage() { function handleSaveImage() {
@ -226,7 +241,7 @@ export function OssFlow() {
}) })
.then(dataURL => { .then(dataURL => {
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute('download', `${controller.schema.alias}.png`); a.setAttribute('download', `${schema.alias}.png`);
a.setAttribute('href', dataURL); a.setAttribute('href', dataURL);
a.click(); a.click();
}) })
@ -246,11 +261,10 @@ export function OssFlow() {
cursorY: event.clientY cursorY: event.clientY
}); });
setIsContextMenuOpen(true); setIsContextMenuOpen(true);
controller.setShowTooltip(false); setHoverOperation(null);
} }
function handleContextMenuHide() { function handleContextMenuHide() {
controller.setShowTooltip(true);
setIsContextMenuOpen(false); setIsContextMenuOpen(false);
} }
@ -262,9 +276,7 @@ export function OssFlow() {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (node.data.operation.result) { if (node.data.operation.result) {
controller.navigateOperationSchema(Number(node.id)); navigateOperationSchema(Number(node.id));
} else {
handleEditOperation(Number(node.id));
} }
} }
@ -272,7 +284,7 @@ export function OssFlow() {
if (isProcessing) { if (isProcessing) {
return; return;
} }
if (!controller.isMutable) { if (!isMutable) {
return; return;
} }
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') { if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
@ -284,7 +296,7 @@ export function OssFlow() {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') { if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleCreateOperation(controller.selected); handleCreateOperation(selected);
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
@ -303,9 +315,9 @@ export function OssFlow() {
> >
<ToolbarOssGraph <ToolbarOssGraph
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })} onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
onCreate={() => handleCreateOperation(controller.selected)} onCreate={() => handleCreateOperation(selected)}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onEdit={() => handleEditOperation(controller.selected[0])} onEdit={() => handleEditOperation(selected[0])}
onExecute={handleExecuteSelected} onExecute={handleExecuteSelected}
onResetPositions={() => setToggleReset(prev => !prev)} onResetPositions={() => setToggleReset(prev => !prev)}
onSavePositions={handleSavePositions} onSavePositions={handleSavePositions}

View File

@ -50,10 +50,10 @@ export function ToolbarOssGraph({
onSavePositions, onSavePositions,
onResetPositions onResetPositions
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const controller = useOssEdit(); const { schema, selected, isMutable, canDelete } = useOssEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const selectedOperation = controller.schema.operationByID.get(controller.selected[0]); const selectedOperation = schema.operationByID.get(selected[0]);
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -70,12 +70,12 @@ export function ToolbarOssGraph({
return false; return false;
} }
const argumentIDs = controller.schema.graph.expandInputs([selectedOperation.id]); const argumentIDs = schema.graph.expandInputs([selectedOperation.id]);
if (!argumentIDs || argumentIDs.length < 1) { if (!argumentIDs || argumentIDs.length < 1) {
return false; return false;
} }
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!); const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) { if (argumentOperations.some(item => item.result === null)) {
return false; return false;
} }
@ -140,7 +140,7 @@ export function ToolbarOssGraph({
offset={4} offset={4}
/> />
</div> </div>
{controller.isMutable ? ( {isMutable ? (
<div className='cc-icons'> <div className='cc-icons'>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
@ -157,19 +157,19 @@ export function ToolbarOssGraph({
<MiniButton <MiniButton
title='Активировать операцию' title='Активировать операцию'
icon={<IconExecute size='1.25rem' className='icon-green' />} icon={<IconExecute size='1.25rem' className='icon-green' />}
disabled={isProcessing || controller.selected.length !== 1 || !readyForSynthesis} disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
onClick={onExecute} onClick={onExecute}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')} titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />} icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
disabled={controller.selected.length !== 1 || isProcessing} disabled={selected.length !== 1 || isProcessing}
onClick={onEdit} onClick={onEdit}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')} titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.selected.length !== 1 || isProcessing || !controller.canDelete(controller.selected[0])} disabled={selected.length !== 1 || isProcessing || !canDelete(selected[0])}
onClick={onDelete} onClick={onDelete}
/> />
</div> </div>

View File

@ -3,20 +3,19 @@
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { IconConsolidation, IconRSForm } from '@/components/Icons'; import { IconConsolidation, IconRSForm } from '@/components/Icons';
import { Indicator } from '@/components/View'; import { Indicator } from '@/components/View';
import { PARAMETER, prefixes } from '@/utils/constants'; import { useTooltipsStore } from '@/stores/tooltips';
import { globals, PARAMETER } from '@/utils/constants';
import { truncateToLastWord } from '@/utils/utils'; import { truncateToLastWord } from '@/utils/utils';
import { OperationType } from '../../../../backend/types'; import { OperationType } from '../../../../backend/types';
import { TooltipOperation } from '../../../../components/TooltipOperation';
import { OssNodeInternal } from '../../../../models/ossLayout'; import { OssNodeInternal } from '../../../../models/ossLayout';
import { useOssEdit } from '../../OssEditContext';
interface NodeCoreProps { interface NodeCoreProps {
node: OssNodeInternal; node: OssNodeInternal;
} }
export function NodeCore({ node }: NodeCoreProps) { export function NodeCore({ node }: NodeCoreProps) {
const controller = useOssEdit(); const setHover = useTooltipsStore(state => state.setActiveOperation);
const hasFile = !!node.data.operation.result; const hasFile = !!node.data.operation.result;
const longLabel = node.data.label.length > PARAMETER.ossLongLabel; const longLabel = node.data.label.length > PARAMETER.ossLongLabel;
@ -29,14 +28,12 @@ export function NodeCore({ node }: NodeCoreProps) {
noPadding noPadding
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'} title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
icon={<IconRSForm className={hasFile ? 'text-ok-600' : 'text-warn-600'} size='12px' />} icon={<IconRSForm className={hasFile ? 'text-ok-600' : 'text-warn-600'} size='12px' />}
hideTitle={!controller.showTooltip}
/> />
{node.data.operation.is_consolidation ? ( {node.data.operation.is_consolidation ? (
<Indicator <Indicator
noPadding noPadding
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент' titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
icon={<IconConsolidation className='text-sec-600' size='12px' />} icon={<IconConsolidation className='text-sec-600' size='12px' />}
hideTitle={!controller.showTooltip}
/> />
) : null} ) : null}
</Overlay> </Overlay>
@ -53,7 +50,11 @@ export function NodeCore({ node }: NodeCoreProps) {
</Overlay> </Overlay>
) : null} ) : null}
<div id={`${prefixes.operation_list}${node.id}`} className='h-[34px] w-[144px] flex items-center justify-center'> <div
className='h-[34px] w-[144px] flex items-center justify-center'
data-tooltip-id={globals.operation_tooltip}
onMouseEnter={() => setHover(node.data.operation)}
>
<div <div
className='text-center' className='text-center'
style={{ style={{
@ -65,9 +66,6 @@ export function NodeCore({ node }: NodeCoreProps) {
> >
{labelText} {labelText}
</div> </div>
{controller.showTooltip && !node.dragging ? (
<TooltipOperation anchor={`#${prefixes.operation_list}${node.id}`} node={node} />
) : null}
</div> </div>
</> </>
); );

View File

@ -29,7 +29,7 @@ import { useMutatingOss } from '../../backend/useMutatingOss';
import { useOssEdit } from './OssEditContext'; import { useOssEdit } from './OssEditContext';
export function MenuOssTabs() { export function MenuOssTabs() {
const controller = useOssEdit(); const { deleteSchema, promptRelocateConstituents, isMutable, isOwned, schema } = useOssEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense(); const { user, isAnonymous } = useAuthSuspense();
@ -44,7 +44,7 @@ export function MenuOssTabs() {
function handleDelete() { function handleDelete() {
schemaMenu.hide(); schemaMenu.hide();
controller.deleteSchema(); deleteSchema();
} }
function handleShare() { function handleShare() {
@ -67,7 +67,7 @@ export function MenuOssTabs() {
function handleRelocate() { function handleRelocate() {
editMenu.hide(); editMenu.hide();
controller.promptRelocateConstituents(undefined, []); promptRelocateConstituents(undefined, []);
} }
return ( return (
@ -90,7 +90,7 @@ export function MenuOssTabs() {
icon={<IconShare size='1rem' className='icon-primary' />} icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare} onClick={handleShare}
/> />
{controller.isMutable ? ( {isMutable ? (
<DropdownButton <DropdownButton
text='Удалить схему' text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
@ -126,7 +126,7 @@ export function MenuOssTabs() {
title='Редактирование' title='Редактирование'
hideTitle={editMenu.isOpen} hideTitle={editMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={controller.isMutable ? 'icon-green' : 'icon-red'} />} icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle} onClick={editMenu.toggle}
/> />
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
@ -175,14 +175,14 @@ export function MenuOssTabs() {
text={labelUserRole(UserRole.EDITOR)} text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)} title={describeUserRole(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />} icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!controller.isOwned && (!user.id || !controller.schema.editors.includes(user.id))} disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
onClick={() => handleChangeRole(UserRole.EDITOR)} onClick={() => handleChangeRole(UserRole.EDITOR)}
/> />
<DropdownButton <DropdownButton
text={labelUserRole(UserRole.OWNER)} text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)} title={describeUserRole(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />} icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!controller.isOwned} disabled={!isOwned}
onClick={() => handleChangeRole(UserRole.OWNER)} onClick={() => handleChangeRole(UserRole.OWNER)}
/> />
<DropdownButton <DropdownButton

View File

@ -4,7 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { ILibraryItemEditor, useDeleteItem, useLibrarySearchStore } from '@/features/library'; import { useDeleteItem, useLibrarySearchStore } from '@/features/library';
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext'; import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
@ -29,16 +29,12 @@ export interface ICreateOperationPrompt {
callback: (newID: number) => void; callback: (newID: number) => void;
} }
export interface IOssEditContext extends ILibraryItemEditor { export interface IOssEditContext {
schema: IOperationSchema; schema: IOperationSchema;
selected: number[]; selected: number[];
isOwned: boolean; isOwned: boolean;
isMutable: boolean; isMutable: boolean;
isAttachedToOSS: boolean;
showTooltip: boolean;
setShowTooltip: (newValue: boolean) => void;
navigateTab: (tab: OssTabID) => void; navigateTab: (tab: OssTabID) => void;
navigateOperationSchema: (target: number) => void; navigateOperationSchema: (target: number) => void;
@ -82,7 +78,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const isOwned = !!user.id && user.id === schema.owner; const isOwned = !!user.id && user.id === schema.owner;
const isMutable = role > UserRole.READER && !schema.read_only; const isMutable = role > UserRole.READER && !schema.read_only;
const [showTooltip, setShowTooltip] = useState(true);
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema); const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
@ -209,12 +204,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
deleteSchema, deleteSchema,
showTooltip,
setShowTooltip,
isOwned, isOwned,
isMutable, isMutable,
isAttachedToOSS: false,
setSelected, setSelected,

View File

@ -1,13 +1,9 @@
'use client'; 'use client';
import { urls, useConceptNavigation } from '@/app';
import { BadgeHelp, HelpTopic } from '@/features/help'; import { BadgeHelp, HelpTopic } from '@/features/help';
import { import { AccessPolicy, LibraryItemType, MiniSelectorOSS, useMutatingLibrary } from '@/features/library';
AccessPolicy, import { ILibraryItem } from '@/features/library/backend/types';
ILibraryItemEditor,
LibraryItemType,
MiniSelectorOSS,
useMutatingLibrary
} from '@/features/library';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
@ -19,33 +15,33 @@ import { tooltipText } from '@/utils/labels';
import { prepareTooltip, sharePage } from '@/utils/utils'; import { prepareTooltip, sharePage } from '@/utils/utils';
import { IRSForm } from '../models/rsform'; import { IRSForm } from '../models/rsform';
import { IRSEditContext } from '../pages/RSFormPage/RSEditContext';
interface ToolbarRSFormCardProps { interface ToolbarRSFormCardProps {
onSubmit: () => void; onSubmit: () => void;
controller: ILibraryItemEditor; isMutable: boolean;
schema: ILibraryItem;
deleteSchema: () => void;
} }
export function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) { export function ToolbarRSFormCard({ schema, onSubmit, isMutable, deleteSchema }: ToolbarRSFormCardProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const router = useConceptNavigation();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useMutatingLibrary(); const isProcessing = useMutatingLibrary();
const canSave = isModified && !isProcessing; const canSave = isModified && !isProcessing;
const ossSelector = (() => { const ossSelector = (() => {
if (controller.schema.item_type !== LibraryItemType.RSFORM) { if (schema.item_type !== LibraryItemType.RSFORM) {
return null; return null;
} }
const schema = controller.schema as IRSForm; const rsSchema = schema as IRSForm;
if (schema.oss.length <= 0) { if (rsSchema.oss.length <= 0) {
return null; return null;
} }
return ( return (
<MiniSelectorOSS <MiniSelectorOSS
items={schema.oss} items={rsSchema.oss}
onSelect={(event, value) => onSelect={(event, value) => router.push(urls.oss(value.id), event.ctrlKey || event.metaKey)}
(controller as IRSEditContext).navigateOss(value.id, event.ctrlKey || event.metaKey)
}
/> />
); );
})(); })();
@ -53,7 +49,7 @@ export function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardPro
return ( return (
<Overlay position='cc-tab-tools' className='cc-icons'> <Overlay position='cc-tab-tools' className='cc-icons'>
{ossSelector} {ossSelector}
{controller.isMutable || isModified ? ( {isMutable || isModified ? (
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave} disabled={!canSave}
@ -62,17 +58,17 @@ export function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardPro
/> />
) : null} ) : null}
<MiniButton <MiniButton
titleHtml={tooltipText.shareItem(controller.schema.access_policy === AccessPolicy.PUBLIC)} titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
icon={<IconShare size='1.25rem' className='icon-primary' />} icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={sharePage} onClick={sharePage}
disabled={controller.schema.access_policy !== AccessPolicy.PUBLIC} disabled={schema.access_policy !== AccessPolicy.PUBLIC}
/> />
{controller.isMutable ? ( {isMutable ? (
<MiniButton <MiniButton
title='Удалить схему' title='Удалить схему'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.isMutable || isProcessing || role < UserRole.OWNER} disabled={!isMutable || isProcessing || role < UserRole.OWNER}
onClick={controller.deleteSchema} onClick={deleteSchema}
/> />
) : null} ) : null}
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} /> <BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />

View File

@ -46,9 +46,20 @@ export function ToolbarConstituenta({
onSubmit, onSubmit,
onReset onReset
}: ToolbarConstituentaProps) { }: ToolbarConstituentaProps) {
const controller = useRSEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { findPredecessor } = useFindPredecessor(); const { findPredecessor } = useFindPredecessor();
const {
schema,
navigateOss,
isContentEditable,
createCst,
createCstDefault,
cloneCst,
canDeleteSelected,
promptDeleteCst,
moveUp,
moveDown
} = useRSEdit();
const showList = usePreferencesStore(state => state.showCstSideList); const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList); const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
@ -72,10 +83,10 @@ export function ToolbarConstituenta({
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2' position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position outline-none cc-blur px-1 rounded-b-2xl' className='cc-icons cc-animate-position outline-none cc-blur px-1 rounded-b-2xl'
> >
{controller.schema.oss.length > 0 ? ( {schema.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS
items={controller.schema.oss} items={schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)} onSelect={(event, value) => navigateOss(value.id, event.ctrlKey || event.metaKey)}
/> />
) : null} ) : null}
{activeCst?.is_inherited ? ( {activeCst?.is_inherited ? (
@ -85,7 +96,7 @@ export function ToolbarConstituenta({
icon={<IconPredecessor size='1.25rem' className='icon-primary' />} icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
/> />
) : null} ) : null}
{controller.isContentEditable ? ( {isContentEditable ? (
<> <>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
@ -102,21 +113,19 @@ export function ToolbarConstituenta({
<MiniButton <MiniButton
title='Создать конституенту после данной' title='Создать конституенту после данной'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
onClick={() => onClick={() => (activeCst ? createCst(activeCst.cst_type, false) : createCstDefault())}
activeCst ? controller.createCst(activeCst.cst_type, false) : controller.createCstDefault()
}
/> />
<MiniButton <MiniButton
titleHtml={isModified ? tooltipText.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={isModified ? tooltipText.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<IconClone size='1.25rem' className='icon-green' />} icon={<IconClone size='1.25rem' className='icon-green' />}
disabled={disabled || isModified} disabled={disabled || isModified}
onClick={controller.cloneCst} onClick={cloneCst}
/> />
<MiniButton <MiniButton
title='Удалить редактируемую конституенту' title='Удалить редактируемую конституенту'
disabled={disabled || !controller.canDeleteSelected} disabled={disabled || !canDeleteSelected}
onClick={controller.promptDeleteCst} onClick={promptDeleteCst}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
/> />
</> </>
@ -128,19 +137,19 @@ export function ToolbarConstituenta({
onClick={toggleList} onClick={toggleList}
/> />
{controller.isContentEditable ? ( {isContentEditable ? (
<> <>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />} icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
disabled={disabled || isModified || controller.schema.items.length < 2} disabled={disabled || isModified || schema.items.length < 2}
onClick={controller.moveUp} onClick={moveUp}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')} titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<IconMoveDown size='1.25rem' className='icon-primary' />} icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
disabled={disabled || isModified || controller.schema.items.length < 2} disabled={disabled || isModified || schema.items.length < 2}
onClick={controller.moveDown} onClick={moveDown}
/> />
</> </>
) : null} ) : null}

View File

@ -56,7 +56,7 @@ export function EditorRSExpression({
onShowTypeGraph, onShowTypeGraph,
...restProps ...restProps
}: EditorRSExpressionProps) { }: EditorRSExpressionProps) {
const controller = useRSEdit(); const { schema } = useRSEdit();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const rsInput = useRef<ReactCodeMirrorRef>(null); const rsInput = useRef<ReactCodeMirrorRef>(null);
@ -78,7 +78,7 @@ export function EditorRSExpression({
alias: activeCst.alias, alias: activeCst.alias,
cst_type: activeCst.cst_type cst_type: activeCst.cst_type
}; };
void checkInternal({ itemID: controller.schema.id, data }).then(parse => { void checkInternal({ itemID: schema.id, data }).then(parse => {
setParseData(parse); setParseData(parse);
onSuccess?.(parse); onSuccess?.(parse);
}); });
@ -179,7 +179,7 @@ export function EditorRSExpression({
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
onAnalyze={handleCheckExpression} onAnalyze={handleCheckExpression}
schema={controller.schema} schema={schema}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
{...restProps} {...restProps}
/> />

View File

@ -15,7 +15,7 @@ import { FormRSForm } from './FormRSForm';
import { RSFormStats } from './RSFormStats'; import { RSFormStats } from './RSFormStats';
export function EditorRSFormCard() { export function EditorRSFormCard() {
const controller = useRSEdit(); const { schema, isArchive, isMutable, deleteSchema, isAttachedToOSS } = useRSEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
function initiateSubmit() { function initiateSubmit() {
@ -36,7 +36,7 @@ export function EditorRSFormCard() {
return ( return (
<> <>
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} /> <ToolbarRSFormCard onSubmit={initiateSubmit} schema={schema} isMutable={isMutable} deleteSchema={deleteSchema} />
<div <div
onKeyDown={handleInput} onKeyDown={handleInput}
className={clsx( className={clsx(
@ -47,10 +47,10 @@ export function EditorRSFormCard() {
> >
<FlexColumn className='flex-shrink'> <FlexColumn className='flex-shrink'>
<FormRSForm /> <FormRSForm />
<EditorLibraryItem controller={controller} /> <EditorLibraryItem schema={schema} isAttachedToOSS={isAttachedToOSS} />
</FlexColumn> </FlexColumn>
<RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> <RSFormStats stats={schema.stats} isArchive={isArchive} />
</div> </div>
</> </>
); );

View File

@ -22,11 +22,11 @@ import { useRSEdit } from '../RSEditContext';
import { ToolbarVersioning } from './ToolbarVersioning'; import { ToolbarVersioning } from './ToolbarVersioning';
export function FormRSForm() { export function FormRSForm() {
const controller = useRSEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { updateItem: updateSchema } = useUpdateItem(); const { updateItem: updateSchema } = useUpdateItem();
const { setIsModified } = useModificationStore(); const { setIsModified } = useModificationStore();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const { schema, isAttachedToOSS, isContentEditable } = useRSEdit();
const { const {
register, register,
@ -38,13 +38,13 @@ export function FormRSForm() {
} = useForm<IUpdateLibraryItemDTO>({ } = useForm<IUpdateLibraryItemDTO>({
resolver: zodResolver(schemaUpdateLibraryItem), resolver: zodResolver(schemaUpdateLibraryItem),
defaultValues: { defaultValues: {
id: controller.schema.id, id: schema.id,
item_type: LibraryItemType.RSFORM, item_type: LibraryItemType.RSFORM,
title: controller.schema.title, title: schema.title,
alias: controller.schema.alias, alias: schema.alias,
comment: controller.schema.comment, comment: schema.comment,
visible: controller.schema.visible, visible: schema.visible,
read_only: controller.schema.read_only read_only: schema.read_only
} }
}); });
const visible = useWatch({ control, name: 'visible' }); const visible = useWatch({ control, name: 'visible' });
@ -55,7 +55,7 @@ export function FormRSForm() {
}, [isDirty, setIsModified]); }, [isDirty, setIsModified]);
function handleSelectVersion(version?: number) { function handleSelectVersion(version?: number) {
router.push(urls.schema(controller.schema.id, version)); router.push(urls.schema(schema.id, version));
} }
function onSubmit(data: IUpdateLibraryItemDTO) { function onSubmit(data: IUpdateLibraryItemDTO) {
@ -73,7 +73,7 @@ export function FormRSForm() {
{...register('title')} {...register('title')}
label='Полное название' label='Полное название'
className='mb-3' className='mb-3'
disabled={!controller.isContentEditable} disabled={!isContentEditable}
error={errors.title} error={errors.title}
/> />
<div className='flex justify-between gap-3 mb-3'> <div className='flex justify-between gap-3 mb-3'>
@ -82,24 +82,25 @@ export function FormRSForm() {
{...register('alias')} {...register('alias')}
label='Сокращение' label='Сокращение'
className='w-[16rem]' className='w-[16rem]'
disabled={!controller.isContentEditable} disabled={!isContentEditable}
error={errors.alias} error={errors.alias}
/> />
<div className='flex flex-col'> <div className='flex flex-col'>
<ToolbarVersioning blockReload={controller.schema.oss.length > 0} /> <ToolbarVersioning blockReload={schema.oss.length > 0} />
<ToolbarItemAccess <ToolbarItemAccess
visible={visible} visible={visible}
toggleVisible={() => setValue('visible', !visible, { shouldDirty: true })} toggleVisible={() => setValue('visible', !visible, { shouldDirty: true })}
readOnly={readOnly} readOnly={readOnly}
toggleReadOnly={() => setValue('read_only', !readOnly, { shouldDirty: true })} toggleReadOnly={() => setValue('read_only', !readOnly, { shouldDirty: true })}
controller={controller} schema={schema}
isAttachedToOSS={isAttachedToOSS}
/> />
<Label text='Версия' className='mb-2 select-none' /> <Label text='Версия' className='mb-2 select-none' />
<SelectVersion <SelectVersion
id='schema_version' id='schema_version'
className='select-none' className='select-none'
value={controller.schema.version} // value={schema.version} //
items={controller.schema.versions} items={schema.versions}
onChange={handleSelectVersion} onChange={handleSelectVersion}
/> />
</div> </div>
@ -110,10 +111,10 @@ export function FormRSForm() {
{...register('comment')} {...register('comment')}
label='Описание' label='Описание'
rows={3} rows={3}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
error={errors.comment} error={errors.comment}
/> />
{controller.isContentEditable || isDirty ? ( {isContentEditable || isDirty ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center mt-4' className='self-center mt-4'

View File

@ -19,18 +19,18 @@ interface ToolbarVersioningProps {
} }
export function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) { export function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
const controller = useRSEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const { versionRestore } = useVersionRestore(); const { versionRestore } = useVersionRestore();
const { schema, isMutable, isContentEditable, navigateVersion, activeVersion, selected } = useRSEdit();
const showCreateVersion = useDialogsStore(state => state.showCreateVersion); const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
const showEditVersions = useDialogsStore(state => state.showEditVersions); const showEditVersions = useDialogsStore(state => state.showEditVersions);
function handleRestoreVersion() { function handleRestoreVersion() {
if (!controller.schema.version || !window.confirm(promptText.restoreArchive)) { if (!schema.version || !window.confirm(promptText.restoreArchive)) {
return; return;
} }
void versionRestore({ versionID: controller.schema.version }).then(() => controller.navigateVersion()); void versionRestore({ versionID: schema.version }).then(() => navigateVersion());
} }
function handleCreateVersion() { function handleCreateVersion() {
@ -38,48 +38,48 @@ export function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
return; return;
} }
showCreateVersion({ showCreateVersion({
itemID: controller.schema.id, itemID: schema.id,
versions: controller.schema.versions, versions: schema.versions,
selected: controller.selected, selected: selected,
totalCount: controller.schema.items.length, totalCount: schema.items.length,
onCreate: newVersion => controller.navigateVersion(newVersion) onCreate: newVersion => navigateVersion(newVersion)
}); });
} }
function handleEditVersions() { function handleEditVersions() {
showEditVersions({ showEditVersions({
itemID: controller.schema.id, itemID: schema.id,
afterDelete: targetVersion => { afterDelete: targetVersion => {
if (targetVersion === controller.activeVersion) controller.navigateVersion(); if (targetVersion === activeVersion) navigateVersion();
} }
}); });
} }
return ( return (
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'> <Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
{controller.isMutable ? ( {isMutable ? (
<> <>
<MiniButton <MiniButton
titleHtml={ titleHtml={
blockReload blockReload
? 'Невозможно откатить КС, <br>прикрепленную к операционной схеме' ? 'Невозможно откатить КС, <br>прикрепленную к операционной схеме'
: !controller.isContentEditable : !isContentEditable
? 'Откатить к версии' ? 'Откатить к версии'
: 'Переключитесь на <br/>неактуальную версию' : 'Переключитесь на <br/>неактуальную версию'
} }
disabled={controller.isContentEditable || blockReload} disabled={isContentEditable || blockReload}
onClick={handleRestoreVersion} onClick={handleRestoreVersion}
icon={<IconUpload size='1.25rem' className='icon-red' />} icon={<IconUpload size='1.25rem' className='icon-red' />}
/> />
<MiniButton <MiniButton
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'} titleHtml={isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
disabled={!controller.isContentEditable} disabled={!isContentEditable}
onClick={handleCreateVersion} onClick={handleCreateVersion}
icon={<IconNewVersion size='1.25rem' className='icon-green' />} icon={<IconNewVersion size='1.25rem' className='icon-green' />}
/> />
<MiniButton <MiniButton
title={controller.schema.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'} title={schema.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
disabled={controller.schema.versions.length === 0} disabled={schema.versions.length === 0}
onClick={handleEditVersions} onClick={handleEditVersions}
icon={<IconVersions size='1.25rem' className='icon-primary' />} icon={<IconVersions size='1.25rem' className='icon-primary' />}
/> />

View File

@ -23,17 +23,30 @@ import { TableRSList } from './TableRSList';
import { ToolbarRSList } from './ToolbarRSList'; import { ToolbarRSList } from './ToolbarRSList';
export function EditorRSList() { export function EditorRSList() {
const controller = useRSEdit();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const {
isContentEditable,
schema,
selected,
deselectAll,
setSelected,
createCst,
createCstDefault,
moveUp,
moveDown,
cloneCst,
canDeleteSelected,
promptDeleteCst,
navigateCst
} = useRSEdit();
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
const filtered = filterText const filtered = filterText
? controller.schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)) ? schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL))
: controller.schema.items; : schema.items;
const rowSelection: RowSelectionState = Object.fromEntries( const rowSelection: RowSelectionState = Object.fromEntries(
filtered.map((cst, index) => [String(index), controller.selected.includes(cst.id)]) filtered.map((cst, index) => [String(index), selected.includes(cst.id)])
); );
function handleDownloadCSV() { function handleDownloadCSV() {
@ -43,7 +56,7 @@ export function EditorRSList() {
} }
const blob = convertToCSV(filtered); const blob = convertToCSV(filtered);
try { try {
fileDownload(blob, `${controller.schema.alias}.csv`, 'text/csv;charset=utf-8;'); fileDownload(blob, `${schema.alias}.csv`, 'text/csv;charset=utf-8;');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -57,26 +70,23 @@ export function EditorRSList() {
newSelection.push(cst.id); newSelection.push(cst.id);
} }
}); });
controller.setSelected(prev => [ setSelected(prev => [...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)), ...newSelection]);
...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)),
...newSelection
]);
} }
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
controller.deselectAll(); deselectAll();
return; return;
} }
if (!controller.isContentEditable || isProcessing) { if (!isContentEditable || isProcessing) {
return; return;
} }
if (event.key === 'Delete' && controller.canDeleteSelected) { if (event.key === 'Delete' && canDeleteSelected) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
controller.promptDeleteCst(); promptDeleteCst();
return; return;
} }
if (!event.altKey || event.shiftKey) { if (!event.altKey || event.shiftKey) {
@ -90,26 +100,26 @@ export function EditorRSList() {
} }
function processAltKey(code: string): boolean { function processAltKey(code: string): boolean {
if (controller.selected.length > 0) { if (selected.length > 0) {
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'ArrowUp': controller.moveUp(); return true; case 'ArrowUp': moveUp(); return true;
case 'ArrowDown': controller.moveDown(); return true; case 'ArrowDown': moveDown(); return true;
case 'KeyV': controller.cloneCst(); return true; case 'KeyV': cloneCst(); return true;
} }
} }
// prettier-ignore // prettier-ignore
switch (code) { switch (code) {
case 'Backquote': controller.createCstDefault(); return true; case 'Backquote': createCstDefault(); return true;
case 'Digit1': controller.createCst(CstType.BASE, true); return true; case 'Digit1': createCst(CstType.BASE, true); return true;
case 'Digit2': controller.createCst(CstType.STRUCTURED, true); return true; case 'Digit2': createCst(CstType.STRUCTURED, true); return true;
case 'Digit3': controller.createCst(CstType.TERM, true); return true; case 'Digit3': createCst(CstType.TERM, true); return true;
case 'Digit4': controller.createCst(CstType.AXIOM, true); return true; case 'Digit4': createCst(CstType.AXIOM, true); return true;
case 'KeyQ': controller.createCst(CstType.FUNCTION, true); return true; case 'KeyQ': createCst(CstType.FUNCTION, true); return true;
case 'KeyW': controller.createCst(CstType.PREDICATE, true); return true; case 'KeyW': createCst(CstType.PREDICATE, true); return true;
case 'Digit5': controller.createCst(CstType.CONSTANT, true); return true; case 'Digit5': createCst(CstType.CONSTANT, true); return true;
case 'Digit6': controller.createCst(CstType.THEOREM, true); return true; case 'Digit6': createCst(CstType.THEOREM, true); return true;
} }
return false; return false;
} }
@ -118,12 +128,12 @@ export function EditorRSList() {
return ( return (
<> <>
{controller.isContentEditable ? <ToolbarRSList /> : null} {isContentEditable ? <ToolbarRSList /> : null}
<div tabIndex={-1} onKeyDown={handleKeyDown} className='cc-fade-in pt-[1.9rem]'> <div tabIndex={-1} onKeyDown={handleKeyDown} className='cc-fade-in pt-[1.9rem]'>
{controller.isContentEditable ? ( {isContentEditable ? (
<div className='flex items-center border-b'> <div className='flex items-center border-b'>
<div className='px-2'> <div className='px-2'>
Выбор {controller.selected.length} из {controller.schema.stats?.count_all} Выбор {selected.length} из {schema.stats?.count_all}
</div> </div>
<SearchBar <SearchBar
id='constituents_search' id='constituents_search'
@ -146,11 +156,11 @@ export function EditorRSList() {
<TableRSList <TableRSList
items={filtered} items={filtered}
maxHeight={tableHeight} maxHeight={tableHeight}
enableSelection={controller.isContentEditable} enableSelection={isContentEditable}
selected={rowSelection} selected={rowSelection}
setSelected={handleRowSelection} setSelected={handleRowSelection}
onEdit={controller.navigateCst} onEdit={navigateCst}
onCreateNew={controller.createCstDefault} onCreateNew={createCstDefault}
/> />
</div> </div>
</> </>

View File

@ -31,46 +31,50 @@ import { getCstTypeShortcut, labelCstType } from '../../../labels';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
export function ToolbarRSList() { export function ToolbarRSList() {
const controller = useRSEdit();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const insertMenu = useDropdown(); const insertMenu = useDropdown();
const {
schema,
selected,
navigateOss,
deselectAll,
createCst,
createCstDefault,
cloneCst,
canDeleteSelected,
promptDeleteCst,
moveUp,
moveDown
} = useRSEdit();
return ( return (
<Overlay <Overlay
position='cc-tab-tools right-4 translate-x-0 md:right-1/2 md:translate-x-1/2' position='cc-tab-tools right-4 translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position items-start outline-none' className='cc-icons cc-animate-position items-start outline-none'
> >
{controller.schema.oss.length > 0 ? ( {schema.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS
items={controller.schema.oss} items={schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)} onSelect={(event, value) => navigateOss(value.id, event.ctrlKey || event.metaKey)}
/> />
) : null} ) : 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' />}
disabled={controller.selected.length === 0} disabled={selected.length === 0}
onClick={controller.deselectAll} onClick={deselectAll}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />} icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
disabled={ disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length}
isProcessing || onClick={moveUp}
controller.selected.length === 0 ||
controller.selected.length === controller.schema.items.length
}
onClick={controller.moveUp}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')} titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<IconMoveDown size='1.25rem' className='icon-primary' />} icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
disabled={ disabled={isProcessing || selected.length === 0 || selected.length === schema.items.length}
isProcessing || onClick={moveDown}
controller.selected.length === 0 ||
controller.selected.length === controller.schema.items.length
}
onClick={controller.moveDown}
/> />
<div ref={insertMenu.ref}> <div ref={insertMenu.ref}>
<MiniButton <MiniButton
@ -86,7 +90,7 @@ export function ToolbarRSList() {
key={`${prefixes.csttype_list}${typeStr}`} key={`${prefixes.csttype_list}${typeStr}`}
text={labelCstType(typeStr as CstType)} text={labelCstType(typeStr as CstType)}
icon={<CstTypeIcon value={typeStr as CstType} size='1.25rem' />} icon={<CstTypeIcon value={typeStr as CstType} size='1.25rem' />}
onClick={() => controller.createCst(typeStr as CstType, true)} onClick={() => createCst(typeStr as CstType, true)}
titleHtml={getCstTypeShortcut(typeStr as CstType)} titleHtml={getCstTypeShortcut(typeStr as CstType)}
/> />
))} ))}
@ -96,19 +100,19 @@ export function ToolbarRSList() {
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')} titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={isProcessing} disabled={isProcessing}
onClick={controller.createCstDefault} onClick={createCstDefault}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<IconClone size='1.25rem' className='icon-green' />} icon={<IconClone size='1.25rem' className='icon-green' />}
disabled={isProcessing || controller.selected.length !== 1} disabled={isProcessing || selected.length !== 1}
onClick={controller.cloneCst} onClick={cloneCst}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')} titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={isProcessing || !controller.canDeleteSelected} disabled={isProcessing || !canDeleteSelected}
onClick={controller.promptDeleteCst} onClick={promptDeleteCst}
/> />
<BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} /> <BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} />
</Overlay> </Overlay>

View File

@ -52,11 +52,21 @@ const ZOOM_MIN = 0.25;
export function TGFlow() { export function TGFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const controller = useRSEdit();
const flow = useReactFlow(); const flow = useReactFlow();
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedNodes } = store.getState(); const { addSelectedNodes } = store.getState();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const {
isContentEditable,
schema,
selected,
setSelected,
navigateCst,
createCst,
toggleSelect,
canDeleteSelected,
promptDeleteCst
} = useRSEdit();
const showParams = useDialogsStore(state => state.showGraphParams); const showParams = useDialogsStore(state => state.showGraphParams);
@ -69,12 +79,12 @@ export function TGFlow() {
const [edges, setEdges] = useEdgesState([]); const [edges, setEdges] = useEdgesState([]);
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null); const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
const filteredGraph = produceFilteredGraph(controller.schema, filter, focusCst); const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
const [hidden, setHidden] = useState<number[]>([]); const [hidden, setHidden] = useState<number[]>([]);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<number | null>(null); const [hoverID, setHoverID] = useState<number | null>(null);
const hoverCst = hoverID && controller.schema.cstByID.get(hoverID); const hoverCst = hoverID && schema.cstByID.get(hoverID);
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay); const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
const [hoverLeft, setHoverLeft] = useState(true); const [hoverLeft, setHoverLeft] = useState(true);
@ -84,9 +94,9 @@ export function TGFlow() {
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => Number(node.id));
if (ids.length === 0) { if (ids.length === 0) {
controller.setSelected([]); setSelected([]);
} else { } else {
controller.setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]); setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
} }
} }
@ -96,24 +106,24 @@ export function TGFlow() {
useEffect(() => { useEffect(() => {
const newDismissed: number[] = []; const newDismissed: number[] = [];
controller.schema.items.forEach(cst => { schema.items.forEach(cst => {
if (!filteredGraph.nodes.has(cst.id)) { if (!filteredGraph.nodes.has(cst.id)) {
newDismissed.push(cst.id); newDismissed.push(cst.id);
} }
}); });
setHidden(newDismissed); setHidden(newDismissed);
setHoverID(null); setHoverID(null);
}, [controller.schema, filteredGraph]); }, [schema, filteredGraph]);
const resetNodes = useCallback(() => { const resetNodes = useCallback(() => {
const newNodes: Node<TGNodeData>[] = []; const newNodes: Node<TGNodeData>[] = [];
filteredGraph.nodes.forEach(node => { filteredGraph.nodes.forEach(node => {
const cst = controller.schema.cstByID.get(node.id); const cst = schema.cstByID.get(node.id);
if (cst) { if (cst) {
newNodes.push({ newNodes.push({
id: String(node.id), id: String(node.id),
type: 'concept', type: 'concept',
selected: controller.selected.includes(node.id), selected: selected.includes(node.id),
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: { data: {
fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring), fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring),
@ -150,11 +160,11 @@ export function TGFlow() {
setNodes(newNodes); setNodes(newNodes);
setEdges(newEdges); setEdges(newEdges);
}, [controller.schema, filteredGraph, setNodes, setEdges, filter.noText, controller.selected, focusCst, coloring]); }, [schema, filteredGraph, setNodes, setEdges, filter.noText, selected, focusCst, coloring]);
useEffect(() => { useEffect(() => {
setNeedReset(true); setNeedReset(true);
}, [controller.schema, focusCst, coloring, filter]); }, [schema, focusCst, coloring, filter]);
useEffect(() => { useEffect(() => {
if (!needReset || !flow.viewportInitialized) { if (!needReset || !flow.viewportInitialized) {
@ -162,7 +172,7 @@ export function TGFlow() {
} }
setNeedReset(false); setNeedReset(false);
resetNodes(); resetNodes();
}, [needReset, controller.schema, resetNodes, flow.viewportInitialized]); }, [needReset, schema, resetNodes, flow.viewportInitialized]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
@ -171,20 +181,20 @@ export function TGFlow() {
}, [toggleResetView, flow, focusCst, filter]); }, [toggleResetView, flow, focusCst, filter]);
function handleSetSelected(newSelection: number[]) { function handleSetSelected(newSelection: number[]) {
controller.setSelected(newSelection); setSelected(newSelection);
addSelectedNodes(newSelection.map(id => String(id))); addSelectedNodes(newSelection.map(id => String(id)));
} }
function handleCreateCst() { function handleCreateCst() {
const definition = controller.selected.map(id => controller.schema.cstByID.get(id)!.alias).join(' '); const definition = selected.map(id => schema.cstByID.get(id)!.alias).join(' ');
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition); createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
} }
function handleDeleteCst() { function handleDeleteCst() {
if (!controller.canDeleteSelected) { if (!canDeleteSelected) {
return; return;
} }
controller.promptDeleteCst(); promptDeleteCst();
} }
function handleSaveImage() { function handleSaveImage() {
@ -210,7 +220,7 @@ export function TGFlow() {
}) })
.then(dataURL => { .then(dataURL => {
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute('download', `${controller.schema.alias}.png`); a.setAttribute('download', `${schema.alias}.png`);
a.setAttribute('href', dataURL); a.setAttribute('href', dataURL);
a.click(); a.click();
}) })
@ -231,7 +241,7 @@ export function TGFlow() {
handleSetSelected([]); handleSetSelected([]);
return; return;
} }
if (!controller.isContentEditable) { if (!isContentEditable) {
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
@ -256,10 +266,10 @@ export function TGFlow() {
if (cstID === null) { if (cstID === null) {
setFocusCst(null); setFocusCst(null);
} else { } else {
const target = controller.schema.cstByID.get(cstID) ?? null; const target = schema.cstByID.get(cstID) ?? null;
setFocusCst(prev => (prev === target ? null : target)); setFocusCst(prev => (prev === target ? null : target));
if (target) { if (target) {
controller.setSelected([]); setSelected([]);
} }
} }
} }
@ -275,7 +285,7 @@ export function TGFlow() {
function handleNodeDoubleClick(event: CProps.EventMouse, cstID: number) { function handleNodeDoubleClick(event: CProps.EventMouse, cstID: number) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
controller.navigateCst(cstID); navigateCst(cstID);
} }
function handleNodeEnter(event: CProps.EventMouse, cstID: number) { function handleNodeEnter(event: CProps.EventMouse, cstID: number) {
@ -307,19 +317,15 @@ export function TGFlow() {
/> />
{!focusCst ? ( {!focusCst ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={controller.schema.graph} graph={schema.graph}
isCore={cstID => { isCore={cstID => {
const cst = controller.schema.cstByID.get(cstID); const cst = schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type); return !!cst && isBasicConcept(cst.cst_type);
}} }}
isOwned={ isOwned={schema.inheritance.length > 0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined}
controller.schema.inheritance.length > 0 value={selected}
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
: undefined
}
value={controller.selected}
onChange={handleSetSelected} onChange={handleSetSelected}
emptySelection={controller.selected.length === 0} emptySelection={selected.length === 0}
/> />
) : null} ) : null}
{focusCst ? ( {focusCst ? (
@ -347,8 +353,8 @@ export function TGFlow() {
<div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}> <div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}>
<SelectedCounter <SelectedCounter
hideZero hideZero
totalCount={controller.schema.stats?.count_all ?? 0} totalCount={schema.stats?.count_all ?? 0}
selectedCount={controller.selected.length} selectedCount={selected.length}
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]' position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]'
/> />
@ -374,13 +380,13 @@ export function TGFlow() {
<Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1 pointer-events-none'> <Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1 pointer-events-none'>
<div className='flex flex-col ml-2 w-[13.5rem]'> <div className='flex flex-col ml-2 w-[13.5rem]'>
<GraphSelectors schema={controller.schema} coloring={coloring} onChangeColoring={setColoring} /> <GraphSelectors schema={schema} coloring={coloring} onChangeColoring={setColoring} />
<ViewHidden <ViewHidden
items={hidden} items={hidden}
selected={controller.selected} selected={selected}
schema={controller.schema} schema={schema}
coloringScheme={coloring} coloringScheme={coloring}
toggleSelection={controller.toggleSelect} toggleSelection={toggleSelect}
setFocus={handleSetFocus} setFocus={handleSetFocus}
/> />
</div> </div>

View File

@ -25,11 +25,11 @@ export function ToolbarFocusedCst({
toggleShowInputs, toggleShowInputs,
toggleShowOutputs toggleShowOutputs
}: ToolbarFocusedCstProps) { }: ToolbarFocusedCstProps) {
const controller = useRSEdit(); const { deselectAll } = useRSEdit();
function resetSelection() { function resetSelection() {
reset(); reset();
controller.setSelected([]); deselectAll();
} }
return ( return (

View File

@ -47,12 +47,12 @@ export function ToolbarTermGraph({
onFitView, onFitView,
onSaveImage onSaveImage
}: ToolbarTermGraphProps) { }: ToolbarTermGraphProps) {
const controller = useRSEdit();
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph); const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const { schema, navigateOss, isContentEditable, canDeleteSelected } = useRSEdit();
function handleShowTypeGraph() { function handleShowTypeGraph() {
const typeInfo = controller.schema.items.map(item => ({ const typeInfo = schema.items.map(item => ({
alias: item.alias, alias: item.alias,
result: item.parse.typification, result: item.parse.typification,
args: item.parse.args args: item.parse.args
@ -62,10 +62,10 @@ export function ToolbarTermGraph({
return ( return (
<div className='cc-icons'> <div className='cc-icons'>
{controller.schema.oss.length > 0 ? ( {schema.oss.length > 0 ? (
<MiniSelectorOSS <MiniSelectorOSS
items={controller.schema.oss} items={schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)} onSelect={(event, value) => navigateOss(value.id, event.ctrlKey || event.metaKey)}
/> />
) : null} ) : null}
<MiniButton <MiniButton
@ -100,7 +100,7 @@ export function ToolbarTermGraph({
} }
onClick={toggleFoldDerived} onClick={toggleFoldDerived}
/> />
{controller.isContentEditable ? ( {isContentEditable ? (
<MiniButton <MiniButton
title='Новая конституента' title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
@ -108,11 +108,11 @@ export function ToolbarTermGraph({
onClick={onCreate} onClick={onCreate}
/> />
) : null} ) : null}
{controller.isContentEditable ? ( {isContentEditable ? (
<MiniButton <MiniButton
title='Удалить выбранные' title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!controller.canDeleteSelected || isProcessing} disabled={!canDeleteSelected || isProcessing}
onClick={onDelete} onClick={onDelete}
/> />
) : null} ) : null}

View File

@ -52,9 +52,21 @@ import { canProduceStructure } from '../../models/rsformAPI';
import { useRSEdit } from './RSEditContext'; import { useRSEdit } from './RSEditContext';
export function MenuRSTabs() { export function MenuRSTabs() {
const controller = useRSEdit();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense(); const { user, isAnonymous } = useAuthSuspense();
const {
activeCst,
schema,
selected,
setSelected,
deleteSchema,
promptTemplate,
deselectAll,
isArchive,
isMutable,
isContentEditable,
isOwned
} = useRSEdit();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole); const setRole = useRoleStore(state => state.setRole);
@ -76,15 +88,15 @@ export function MenuRSTabs() {
const editMenu = useDropdown(); const editMenu = useDropdown();
const accessMenu = useDropdown(); const accessMenu = useDropdown();
const structureEnabled = !!controller.activeCst && canProduceStructure(controller.activeCst); const structureEnabled = !!activeCst && canProduceStructure(activeCst);
function calculateCloneLocation() { function calculateCloneLocation() {
const location = controller.schema.location; const location = schema.location;
const head = location.substring(0, 2) as LocationHead; const head = location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) { if (head === LocationHead.LIBRARY) {
return user.is_staff ? location : LocationHead.USER; return user.is_staff ? location : LocationHead.USER;
} }
if (controller.schema.owner === user.id) { if (schema.owner === user.id) {
return location; return location;
} }
return head === LocationHead.USER ? LocationHead.USER : location; return head === LocationHead.USER ? LocationHead.USER : location;
@ -92,7 +104,7 @@ export function MenuRSTabs() {
function handleDelete() { function handleDelete() {
schemaMenu.hide(); schemaMenu.hide();
controller.deleteSchema(); deleteSchema();
} }
function handleDownload() { function handleDownload() {
@ -100,10 +112,10 @@ export function MenuRSTabs() {
if (isModified && !promptUnsaved()) { if (isModified && !promptUnsaved()) {
return; return;
} }
const fileName = (controller.schema.alias ?? 'Schema') + EXTEOR_TRS_FILE; const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
void download({ void download({
itemID: controller.schema.id, itemID: schema.id,
version: controller.schema.version version: schema.version
}).then((data: Blob) => { }).then((data: Blob) => {
try { try {
fileDownload(data, fileName); fileDownload(data, fileName);
@ -115,7 +127,7 @@ export function MenuRSTabs() {
function handleUpload() { function handleUpload() {
schemaMenu.hide(); schemaMenu.hide();
showUpload({ itemID: controller.schema.id }); showUpload({ itemID: schema.id });
} }
function handleClone() { function handleClone() {
@ -124,10 +136,10 @@ export function MenuRSTabs() {
return; return;
} }
showClone({ showClone({
base: controller.schema, base: schema,
initialLocation: calculateCloneLocation(), initialLocation: calculateCloneLocation(),
selected: controller.selected, selected: selected,
totalCount: controller.schema.items.length totalCount: schema.items.length
}); });
} }
@ -143,12 +155,12 @@ export function MenuRSTabs() {
function handleReindex() { function handleReindex() {
editMenu.hide(); editMenu.hide();
void resetAliases({ itemID: controller.schema.id }); void resetAliases({ itemID: schema.id });
} }
function handleRestoreOrder() { function handleRestoreOrder() {
editMenu.hide(); editMenu.hide();
void restoreOrder({ itemID: controller.schema.id }); void restoreOrder({ itemID: schema.id });
} }
function handleSubstituteCst() { function handleSubstituteCst() {
@ -157,31 +169,30 @@ export function MenuRSTabs() {
return; return;
} }
showSubstituteCst({ showSubstituteCst({
schema: controller.schema, schema: schema,
onSubstitute: data => onSubstitute: data => setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)))
controller.setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)))
}); });
} }
function handleTemplates() { function handleTemplates() {
editMenu.hide(); editMenu.hide();
controller.promptTemplate(); promptTemplate();
} }
function handleProduceStructure() { function handleProduceStructure() {
editMenu.hide(); editMenu.hide();
if (!controller.activeCst) { if (!activeCst) {
return; return;
} }
if (isModified && !promptUnsaved()) { if (isModified && !promptUnsaved()) {
return; return;
} }
void produceStructure({ void produceStructure({
itemID: controller.schema.id, itemID: schema.id,
data: { target: controller.activeCst.id } data: { target: activeCst.id }
}).then(cstList => { }).then(cstList => {
if (cstList.length !== 0) { if (cstList.length !== 0) {
controller.setSelected(cstList); setSelected(cstList);
} }
}); });
} }
@ -192,8 +203,8 @@ export function MenuRSTabs() {
return; return;
} }
showInlineSynthesis({ showInlineSynthesis({
receiver: controller.schema, receiver: schema,
onSynthesis: () => controller.deselectAll() onSynthesis: () => deselectAll()
}); });
} }
@ -227,10 +238,10 @@ export function MenuRSTabs() {
<Dropdown isOpen={schemaMenu.isOpen}> <Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton <DropdownButton
text='Поделиться' text='Поделиться'
titleHtml={tooltipText.shareItem(controller.schema.access_policy === AccessPolicy.PUBLIC)} titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
icon={<IconShare size='1rem' className='icon-primary' />} icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare} onClick={handleShare}
disabled={controller.schema.access_policy !== AccessPolicy.PUBLIC} disabled={schema.access_policy !== AccessPolicy.PUBLIC}
/> />
<DropdownButton <DropdownButton
text='QR-код' text='QR-код'
@ -242,7 +253,7 @@ export function MenuRSTabs() {
<DropdownButton <DropdownButton
text='Клонировать' text='Клонировать'
icon={<IconClone size='1rem' className='icon-green' />} icon={<IconClone size='1rem' className='icon-green' />}
disabled={controller.isArchive} disabled={isArchive}
onClick={handleClone} onClick={handleClone}
/> />
) : null} ) : null}
@ -251,15 +262,15 @@ export function MenuRSTabs() {
icon={<IconDownload size='1rem' className='icon-primary' />} icon={<IconDownload size='1rem' className='icon-primary' />}
onClick={handleDownload} onClick={handleDownload}
/> />
{controller.isContentEditable ? ( {isContentEditable ? (
<DropdownButton <DropdownButton
text='Загрузить из Экстеор' text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />} icon={<IconUpload size='1rem' className='icon-red' />}
disabled={isProcessing || controller.schema.oss.length !== 0} disabled={isProcessing || schema.oss.length !== 0}
onClick={handleUpload} onClick={handleUpload}
/> />
) : null} ) : null}
{controller.isMutable ? ( {isMutable ? (
<DropdownButton <DropdownButton
text='Удалить схему' text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
@ -277,11 +288,11 @@ export function MenuRSTabs() {
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
) : null} ) : null}
{controller.schema.oss.length > 0 ? ( {schema.oss.length > 0 ? (
<DropdownButton <DropdownButton
text='Перейти к ОСС' text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />} icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(controller.schema.oss[0].id))} onClick={() => router.push(urls.oss(schema.oss[0].id))}
/> />
) : null} ) : null}
<DropdownButton <DropdownButton
@ -291,7 +302,7 @@ export function MenuRSTabs() {
/> />
</Dropdown> </Dropdown>
</div> </div>
{!controller.isArchive && !isAnonymous ? ( {!isArchive && !isAnonymous ? (
<div ref={editMenu.ref}> <div ref={editMenu.ref}>
<Button <Button
dense dense
@ -301,7 +312,7 @@ export function MenuRSTabs() {
title='Редактирование' title='Редактирование'
hideTitle={editMenu.isOpen} hideTitle={editMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />} icon={<IconEdit2 size='1.25rem' className={isContentEditable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle} onClick={editMenu.toggle}
/> />
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
@ -309,14 +320,14 @@ export function MenuRSTabs() {
text='Шаблоны' text='Шаблоны'
title='Создать конституенту из шаблона' title='Создать конституенту из шаблона'
icon={<IconTemplates size='1rem' className='icon-green' />} icon={<IconTemplates size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
onClick={handleTemplates} onClick={handleTemplates}
/> />
<DropdownButton <DropdownButton
text='Встраивание' text='Встраивание'
titleHtml='Импортировать совокупность <br/>конституент из другой схемы' titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
icon={<IconInlineSynthesis size='1rem' className='icon-green' />} icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
onClick={handleInlineSynthesis} onClick={handleInlineSynthesis}
/> />
@ -326,21 +337,21 @@ export function MenuRSTabs() {
text='Упорядочить список' text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент' titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
icon={<IconSortList size='1rem' className='icon-primary' />} icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
onClick={handleRestoreOrder} onClick={handleRestoreOrder}
/> />
<DropdownButton <DropdownButton
text='Порядковые имена' text='Порядковые имена'
titleHtml='Присвоить порядковые имена <br/>и обновить выражения' titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
icon={<IconGenerateNames size='1rem' className='icon-primary' />} icon={<IconGenerateNames size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
onClick={handleReindex} onClick={handleReindex}
/> />
<DropdownButton <DropdownButton
text='Порождение структуры' text='Порождение структуры'
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты' titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
icon={<IconGenerateStructure size='1rem' className='icon-primary' />} icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || !structureEnabled || isProcessing} disabled={!isContentEditable || !structureEnabled || isProcessing}
onClick={handleProduceStructure} onClick={handleProduceStructure}
/> />
<DropdownButton <DropdownButton
@ -348,12 +359,12 @@ export function MenuRSTabs() {
titleHtml='Заменить вхождения <br/>одной конституенты на другую' titleHtml='Заменить вхождения <br/>одной конституенты на другую'
icon={<IconReplace size='1rem' className='icon-red' />} icon={<IconReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst} onClick={handleSubstituteCst}
disabled={!controller.isContentEditable || isProcessing} disabled={!isContentEditable || isProcessing}
/> />
</Dropdown> </Dropdown>
</div> </div>
) : null} ) : null}
{controller.isArchive && !isAnonymous ? ( {isArchive && !isAnonymous ? (
<Button <Button
dense dense
noBorder noBorder
@ -363,7 +374,7 @@ export function MenuRSTabs() {
hideTitle={accessMenu.isOpen} hideTitle={accessMenu.isOpen}
className='h-full px-2' className='h-full px-2'
icon={<IconArchive size='1.25rem' className='icon-primary' />} icon={<IconArchive size='1.25rem' className='icon-primary' />}
onClick={event => router.push(urls.schema(controller.schema.id), event.ctrlKey || event.metaKey)} onClick={event => router.push(urls.schema(schema.id), event.ctrlKey || event.metaKey)}
/> />
) : null} ) : null}
{!isAnonymous ? ( {!isAnonymous ? (
@ -400,14 +411,14 @@ export function MenuRSTabs() {
text={labelAccessMode(UserRole.EDITOR)} text={labelAccessMode(UserRole.EDITOR)}
title={describeAccessMode(UserRole.EDITOR)} title={describeAccessMode(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />} icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!controller.isOwned && (!user.id || !controller.schema.editors.includes(user.id))} disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
onClick={() => handleChangeMode(UserRole.EDITOR)} onClick={() => handleChangeMode(UserRole.EDITOR)}
/> />
<DropdownButton <DropdownButton
text={labelAccessMode(UserRole.OWNER)} text={labelAccessMode(UserRole.OWNER)}
title={describeAccessMode(UserRole.OWNER)} title={describeAccessMode(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />} icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!controller.isOwned} disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)} onClick={() => handleChangeMode(UserRole.OWNER)}
/> />
<DropdownButton <DropdownButton

View File

@ -4,7 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { ILibraryItemEditor, useDeleteItem, useLibrarySearchStore } from '@/features/library'; import { useDeleteItem, useLibrarySearchStore } from '@/features/library';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -28,7 +28,7 @@ export enum RSTabID {
TERM_GRAPH = 3 TERM_GRAPH = 3
} }
export interface IRSEditContext extends ILibraryItemEditor { export interface IRSEditContext {
schema: IRSForm; schema: IRSForm;
selected: number[]; selected: number[];
activeCst: IConstituenta | null; activeCst: IConstituenta | null;

View File

@ -1,13 +1,19 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { IOperation } from '@/features/oss/models/oss';
import { IConstituenta } from '@/features/rsform/models/rsform'; import { IConstituenta } from '@/features/rsform/models/rsform';
interface TooltipsStore { interface TooltipsStore {
activeCst: IConstituenta | null; activeCst: IConstituenta | null;
setActiveCst: (value: IConstituenta | null) => void; setActiveCst: (value: IConstituenta | null) => void;
activeOperation: IOperation | null;
setActiveOperation: (value: IOperation | null) => void;
} }
export const useTooltipsStore = create<TooltipsStore>()(set => ({ export const useTooltipsStore = create<TooltipsStore>()(set => ({
activeCst: null, activeCst: null,
setActiveCst: value => set({ activeCst: value }) setActiveCst: value => set({ activeCst: value }),
activeOperation: null,
setActiveOperation: value => set({ activeOperation: value })
})); }));

View File

@ -111,8 +111,8 @@ export const globals = {
tooltip: 'global_tooltip', tooltip: 'global_tooltip',
value_tooltip: 'value_tooltip', value_tooltip: 'value_tooltip',
constituenta_tooltip: 'cst_tooltip', constituenta_tooltip: 'cst_tooltip',
operation_tooltip: 'operation_tooltip',
email_tooltip: 'email_tooltip', email_tooltip: 'email_tooltip',
main_scroll: 'main_scroll',
library_item_editor: 'library_item_editor', library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor', constituenta_editor: 'constituenta_editor',
graph_schemas: 'graph_schemas_tooltip' graph_schemas: 'graph_schemas_tooltip'