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-zoom-pan-pinch
- react-hook-form
- react-scan
- reactflow
- js-file-download
- use-debounce

View File

@ -12,6 +12,8 @@
<meta name="google-site-verification" content="bodB0xvBD_xM-VHg7EgfTf87jEMBF1DriZKdrZjwW1k" />
<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.gstatic.com" crossorigin />
<link

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,14 +1,23 @@
'use client';
import { InfoConstituenta } from '@/features/rsform/components/InfoConstituenta';
import React, { Suspense } from 'react';
import { Tooltip } from '@/components/Container';
import { Loader } from '@/components/Loader';
import { useTooltipsStore } from '@/stores/tooltips';
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 = () => {
const hoverCst = useTooltipsStore(state => state.activeCst);
const hoverOperation = useTooltipsStore(state => state.activeOperation);
return (
<>
@ -25,8 +34,26 @@ export const GlobalTooltips = () => {
layer='z-topmost'
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
/>
<Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'>
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />}
<Tooltip
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>
</>
);

View File

@ -1,9 +1,8 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router';
import { createContext, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { globals } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels';
interface INavigationContext {
@ -29,68 +28,48 @@ export const useConceptNavigation = () => {
export const NavigationState = ({ children }: React.PropsWithChildren) => {
const router = useNavigate();
const { pathname } = useLocation();
const [isBlocked, setIsBlocked] = useState(false);
const validate = useCallback(() => {
function validate() {
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(() => {
window.scrollTo(0, 0);
const mainScroll = document.getElementById(globals.main_scroll);
if (mainScroll) {
mainScroll.scroll(0, 0);
function push(path: string, newTab?: boolean) {
if (newTab) {
window.open(`${path}`, '_blank');
return;
}
}, []);
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()) {
scrollTop();
Promise.resolve(router(-1)).catch(console.log);
Promise.resolve(router(path, { viewTransition: true })).catch(console.error);
setIsBlocked(false);
}
}, [router, validate, scrollTop]);
}
const forward = useCallback(() => {
function replace(path: string) {
if (validate()) {
scrollTop();
Promise.resolve(router(1)).catch(console.log);
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.error);
setIsBlocked(false);
}
}, [router, validate, scrollTop]);
}
useEffect(() => {
scrollTop();
}, [pathname, scrollTop]);
function back() {
if (validate()) {
Promise.resolve(router(-1)).catch(console.error);
setIsBlocked(false);
}
}
function forward() {
if (validate()) {
Promise.resolve(router(1)).catch(console.error);
setIsBlocked(false);
}
}
return (
<NavigationContext

View File

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

View File

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

View File

@ -8,11 +8,10 @@ import { IconImmutable, IconMutable } from '@/components/Icons';
import { Label } from '@/components/Input';
import { PARAMETER } from '@/utils/constants';
import { AccessPolicy } from '../backend/types';
import { AccessPolicy, ILibraryItem } from '../backend/types';
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
import { ILibraryItemEditor } from './EditorLibraryItem';
import { SelectAccessPolicy } from './SelectAccessPolicy';
interface ToolbarItemAccessProps {
@ -20,7 +19,8 @@ interface ToolbarItemAccessProps {
toggleVisible: () => void;
readOnly: boolean;
toggleReadOnly: () => void;
controller: ILibraryItemEditor;
schema: ILibraryItem;
isAttachedToOSS: boolean;
}
export function ToolbarItemAccess({
@ -28,15 +28,16 @@ export function ToolbarItemAccess({
toggleVisible,
readOnly,
toggleReadOnly,
controller
schema,
isAttachedToOSS
}: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role);
const isProcessing = useMutatingLibrary();
const policy = controller.schema.access_policy;
const policy = schema.access_policy;
const { setAccessPolicy } = useSetAccessPolicy();
function handleSetAccessPolicy(newPolicy: AccessPolicy) {
void setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy });
void setAccessPolicy({ itemID: schema.id, policy: newPolicy });
}
return (
@ -44,7 +45,7 @@ export function ToolbarItemAccess({
<Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'>
<SelectAccessPolicy
disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS}
disabled={role <= UserRole.EDITOR || isProcessing || isAttachedToOSS}
value={policy}
onChange={handleSetAccessPolicy}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
import { urls, useConceptNavigation } from '@/app';
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 { useRoleStore, UserRole } from '@/features/users';
@ -29,16 +29,12 @@ export interface ICreateOperationPrompt {
callback: (newID: number) => void;
}
export interface IOssEditContext extends ILibraryItemEditor {
export interface IOssEditContext {
schema: IOperationSchema;
selected: number[];
isOwned: boolean;
isMutable: boolean;
isAttachedToOSS: boolean;
showTooltip: boolean;
setShowTooltip: (newValue: boolean) => void;
navigateTab: (tab: OssTabID) => 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 isMutable = role > UserRole.READER && !schema.read_only;
const [showTooltip, setShowTooltip] = useState(true);
const [selected, setSelected] = useState<number[]>([]);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
@ -209,12 +204,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
deleteSchema,
showTooltip,
setShowTooltip,
isOwned,
isMutable,
isAttachedToOSS: false,
setSelected,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { createContext, useContext, useEffect, useState } from 'react';
import { urls, useConceptNavigation } from '@/app';
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 { useDialogsStore } from '@/stores/dialogs';
@ -28,7 +28,7 @@ export enum RSTabID {
TERM_GRAPH = 3
}
export interface IRSEditContext extends ILibraryItemEditor {
export interface IRSEditContext {
schema: IRSForm;
selected: number[];
activeCst: IConstituenta | null;

View File

@ -1,13 +1,19 @@
import { create } from 'zustand';
import { IOperation } from '@/features/oss/models/oss';
import { IConstituenta } from '@/features/rsform/models/rsform';
interface TooltipsStore {
activeCst: IConstituenta | null;
setActiveCst: (value: IConstituenta | null) => void;
activeOperation: IOperation | null;
setActiveOperation: (value: IOperation | null) => void;
}
export const useTooltipsStore = create<TooltipsStore>()(set => ({
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',
value_tooltip: 'value_tooltip',
constituenta_tooltip: 'cst_tooltip',
operation_tooltip: 'operation_tooltip',
email_tooltip: 'email_tooltip',
main_scroll: 'main_scroll',
library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor',
graph_schemas: 'graph_schemas_tooltip'