F: Add side panel for schema preview pt1
This commit is contained in:
parent
ac38e9f4b5
commit
8e474160d5
|
@ -10,8 +10,9 @@ export const GlobalTooltips = () => {
|
|||
float
|
||||
id={globalIDs.tooltip}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-80 break-words rounded-lg! select-none'
|
||||
place='bottom-start'
|
||||
offset={24}
|
||||
className='max-w-80 break-words rounded-lg! select-none'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconLeftOpen,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconNewItem,
|
||||
|
@ -30,7 +31,7 @@ export function HelpOssGraph() {
|
|||
<div className='flex flex-col'>
|
||||
<h1 className='sm:pr-24'>Граф синтеза</h1>
|
||||
<div className='flex flex-col sm:flex-row'>
|
||||
<div className='sm:w-56'>
|
||||
<div className='sm:w-64'>
|
||||
<h2>Настройка графа</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -39,6 +40,9 @@ export function HelpOssGraph() {
|
|||
<li>
|
||||
<IconFitImage className='inline-icon' /> Вписать в экран
|
||||
</li>
|
||||
<li>
|
||||
<IconLeftOpen className='inline-icon' /> Панель связанной КС
|
||||
</li>
|
||||
<li>
|
||||
<IconSettings className='inline-icon' /> Диалог настроек
|
||||
</li>
|
||||
|
@ -66,7 +70,7 @@ export function HelpOssGraph() {
|
|||
|
||||
<Divider vertical margins='mx-3 mt-3' className='hidden sm:block' />
|
||||
|
||||
<div className='sm:w-84'>
|
||||
<div className='sm:w-76'>
|
||||
<h2>Изменение узлов</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -97,7 +101,7 @@ export function HelpOssGraph() {
|
|||
<Divider margins='my-2' className='hidden sm:block' />
|
||||
|
||||
<div className='flex flex-col-reverse mb-3 sm:flex-row'>
|
||||
<div className='sm:w-56'>
|
||||
<div className='sm:w-64'>
|
||||
<h2>Общие</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -114,7 +118,7 @@ export function HelpOssGraph() {
|
|||
|
||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||
|
||||
<div className='dense w-84'>
|
||||
<div className='dense w-76'>
|
||||
<h2>Контекстное меню</h2>
|
||||
<ul>
|
||||
<li>
|
||||
|
|
|
@ -15,9 +15,9 @@ export function IconShowSidebar({
|
|||
}
|
||||
} else {
|
||||
if (value) {
|
||||
return <IconLeftClose size={size} className={className ?? 'icon-primary'} />;
|
||||
} else {
|
||||
return <IconLeftOpen size={size} className={className ?? 'icon-primary'} />;
|
||||
} else {
|
||||
return <IconLeftClose size={size} className={className ?? 'icon-primary'} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import clsx from 'clsx';
|
|||
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
|
@ -23,6 +24,7 @@ import { useContextMenu } from './context-menu/use-context-menu';
|
|||
import { OssNodeTypes } from './graph/oss-node-types';
|
||||
import { CoordinateDisplay } from './coordinate-display';
|
||||
import { useOssFlow } from './oss-flow-context';
|
||||
import { SidePanel } from './side-panel';
|
||||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||
import { useDragging } from './use-dragging';
|
||||
import { useGetLayout } from './use-get-layout';
|
||||
|
@ -52,6 +54,7 @@ export function OssFlow() {
|
|||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const showPanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
|
||||
const getLayout = useGetLayout();
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
|
@ -225,6 +228,16 @@ export function OssFlow() {
|
|||
onNodeDrag={handleDrag}
|
||||
onNodeDragStop={handleDragStop}
|
||||
/>
|
||||
|
||||
<SidePanel
|
||||
className={clsx(
|
||||
'absolute right-0 top-0 z-sticky w-84 min-h-80',
|
||||
'cc-animate-panel cc-shadow-border',
|
||||
showPanel ? 'translate-x-0' : 'opacity-0 translate-x-full pointer-events-none'
|
||||
)}
|
||||
isMounted={showPanel}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { SidePanel } from './side-panel';
|
|
@ -0,0 +1,72 @@
|
|||
import { Suspense } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClose } from '@/components/icons';
|
||||
import { Loader } from '@/components/loader';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { type IOssItem, NodeType } from '../../../../models/oss';
|
||||
|
||||
import { ViewSchema } from './view-schema';
|
||||
|
||||
interface SidePanelProps {
|
||||
selectedItems: IOssItem[];
|
||||
className?: string;
|
||||
isMounted: boolean;
|
||||
}
|
||||
|
||||
export function SidePanel({ selectedItems, isMounted, className }: SidePanelProps) {
|
||||
const selectedOperation =
|
||||
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
|
||||
const selectedSchema = selectedOperation?.result ?? null;
|
||||
|
||||
const debouncedMounted = useDebounce(isMounted, PARAMETER.moveDuration);
|
||||
const closePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
||||
const sidePanelHeight = useMainHeight();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col py-2 h-full overflow-hidden',
|
||||
'border-l rounded-none rounded-l-sm bg-background',
|
||||
className
|
||||
)}
|
||||
style={{ height: sidePanelHeight }}
|
||||
>
|
||||
<MiniButton
|
||||
titleHtml='Закрыть панель'
|
||||
aria-label='Закрыть'
|
||||
noPadding
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='absolute z-pop top-2 right-1'
|
||||
onClick={closePanel}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'mt-0 mb-1',
|
||||
'font-medium text-sm select-none self-center',
|
||||
'transition-transform',
|
||||
selectedSchema && 'translate-x-16'
|
||||
)}
|
||||
>
|
||||
Содержание КС
|
||||
</div>
|
||||
|
||||
{!selectedOperation ? (
|
||||
<div className='text-center text-sm cc-fade-in'>Выделите операцию для просмотра</div>
|
||||
) : !selectedSchema ? (
|
||||
<div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div>
|
||||
) : debouncedMounted ? (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<ViewSchema schemaID={selectedSchema} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||
import { CstType, type IConstituentaBasicsDTO, type ICreateConstituentaDTO } from '@/features/rsform/backend/types';
|
||||
import { useCreateConstituenta } from '@/features/rsform/backend/use-create-constituenta';
|
||||
import { useMoveConstituents } from '@/features/rsform/backend/use-move-constituents';
|
||||
import { useMutatingRSForm } from '@/features/rsform/backend/use-mutating-rsform';
|
||||
import { generateAlias } from '@/features/rsform/models/rsform-api';
|
||||
import { useCstSearchStore } from '@/features/rsform/stores/cst-search';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClone, IconDestroy, IconMoveDown, IconMoveUp, IconNewItem, IconRSForm } from '@/components/icons';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||
import { type RO } from '@/utils/meta';
|
||||
|
||||
interface ToolbarConstituentsProps {
|
||||
schema: IRSForm;
|
||||
activeCst: IConstituenta | null;
|
||||
setActive: (cstID: number) => void;
|
||||
resetActive: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ToolbarConstituents({
|
||||
schema,
|
||||
activeCst,
|
||||
setActive,
|
||||
resetActive,
|
||||
className
|
||||
}: ToolbarConstituentsProps) {
|
||||
const router = useConceptNavigation();
|
||||
const isProcessing = useMutatingRSForm();
|
||||
const searchText = useCstSearchStore(state => state.query);
|
||||
const hasSearch = searchText.length > 0;
|
||||
|
||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||
const { moveConstituents } = useMoveConstituents();
|
||||
const { createConstituenta } = useCreateConstituenta();
|
||||
|
||||
function navigateRSForm() {
|
||||
router.push({ path: urls.schema(schema.id) });
|
||||
}
|
||||
|
||||
function onCreateCst(newCst: RO<IConstituentaBasicsDTO>) {
|
||||
setActive(newCst.id);
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.id}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
}, PARAMETER.refreshTimeout);
|
||||
}
|
||||
|
||||
function createCst() {
|
||||
const targetType = activeCst?.cst_type ?? CstType.BASE;
|
||||
const data: ICreateConstituentaDTO = {
|
||||
insert_after: activeCst?.id ?? null,
|
||||
cst_type: targetType,
|
||||
alias: generateAlias(targetType, schema),
|
||||
term_raw: '',
|
||||
definition_formal: '',
|
||||
definition_raw: '',
|
||||
convention: '',
|
||||
term_forms: []
|
||||
};
|
||||
showCreateCst({ schema: schema, onCreate: onCreateCst, initial: data });
|
||||
}
|
||||
|
||||
function cloneCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
void createConstituenta({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
insert_after: activeCst.id,
|
||||
cst_type: activeCst.cst_type,
|
||||
alias: generateAlias(activeCst.cst_type, schema),
|
||||
term_raw: activeCst.term_raw,
|
||||
definition_formal: activeCst.definition_formal,
|
||||
definition_raw: activeCst.definition_raw,
|
||||
convention: activeCst.convention,
|
||||
term_forms: activeCst.term_forms
|
||||
}
|
||||
}).then(onCreateCst);
|
||||
}
|
||||
|
||||
function promptDeleteCst() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
showDeleteCst({
|
||||
schema: schema,
|
||||
selected: [activeCst.id],
|
||||
afterDelete: resetActive
|
||||
});
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (activeCst.id !== cst.id) {
|
||||
return prev;
|
||||
} else if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.min(prev, index);
|
||||
}, -1);
|
||||
const target = Math.max(0, currentIndex - 1);
|
||||
void moveConstituents({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
items: [activeCst.id],
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (!activeCst) {
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||
if (activeCst.id !== cst.id) {
|
||||
return prev;
|
||||
} else {
|
||||
count += 1;
|
||||
if (prev === -1) {
|
||||
return index;
|
||||
}
|
||||
return Math.max(prev, index);
|
||||
}
|
||||
}, -1);
|
||||
const target = Math.min(schema.items.length - 1, currentIndex - count + 2);
|
||||
void moveConstituents({
|
||||
itemID: schema.id,
|
||||
data: {
|
||||
items: [activeCst.id],
|
||||
move_to: target
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-0.5', className)}>
|
||||
<MiniButton
|
||||
title='Перейти к концептуальной схеме'
|
||||
icon={<IconRSForm size='1rem' className='icon-primary' />}
|
||||
onClick={navigateRSForm}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Создать конституенту'
|
||||
icon={<IconNewItem size='1rem' className='icon-green' />}
|
||||
onClick={createCst}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Клонировать конституенту'
|
||||
icon={<IconClone size='1rem' className='icon-green' />}
|
||||
onClick={cloneCst}
|
||||
disabled={!activeCst || isProcessing}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Удалить выделенную конституенту'
|
||||
onClick={promptDeleteCst}
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={!activeCst || isProcessing || activeCst?.is_inherited}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Переместить вверх'
|
||||
icon={<IconMoveUp size='1rem' className='icon-primary' />}
|
||||
onClick={moveUp}
|
||||
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить вниз'
|
||||
icon={<IconMoveDown size='1rem' className='icon-primary' />}
|
||||
onClick={moveDown}
|
||||
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useRSFormSuspense } from '@/features/rsform/backend/use-rsform';
|
||||
import { RSFormStats } from '@/features/rsform/components/rsform-stats';
|
||||
import { ViewConstituents } from '@/features/rsform/components/view-constituents';
|
||||
|
||||
import { useFitHeight } from '@/stores/app-layout';
|
||||
|
||||
import { ToolbarConstituents } from './toolbar-constituents';
|
||||
|
||||
interface ViewSchemaProps {
|
||||
schemaID: number;
|
||||
}
|
||||
|
||||
export function ViewSchema({ schemaID }: ViewSchemaProps) {
|
||||
const { schema } = useRSFormSuspense({ itemID: schemaID });
|
||||
const [activeID, setActiveID] = useState<number | null>(null);
|
||||
const activeCst = activeID ? schema.cstByID.get(activeID) ?? null : null;
|
||||
|
||||
const listHeight = useFitHeight('14.5rem', '10rem');
|
||||
|
||||
return (
|
||||
<div className='grid h-full relative cc-fade-in' style={{ gridTemplateRows: '1fr auto' }}>
|
||||
<ToolbarConstituents
|
||||
className='absolute -top-7 left-1'
|
||||
schema={schema}
|
||||
activeCst={activeCst}
|
||||
setActive={setActiveID}
|
||||
resetActive={() => setActiveID(null)}
|
||||
/>
|
||||
|
||||
<ViewConstituents
|
||||
dense
|
||||
noBorder
|
||||
className='border-y rounded-none'
|
||||
schema={schema}
|
||||
activeCst={activeCst}
|
||||
onActivate={cst => setActiveID(cst.id)}
|
||||
maxListHeight={listHeight}
|
||||
/>
|
||||
|
||||
<RSFormStats className='pr-4 py-2 ml-[-1rem]' stats={schema.stats} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
import { IconShowSidebar } from '@/features/library/components/icon-show-sidebar';
|
||||
import { type OssNode } from '@/features/oss/models/oss-layout';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
|
@ -20,6 +21,7 @@ import {
|
|||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { isIOS, prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
|
@ -64,10 +66,12 @@ export function ToolbarOssGraph({
|
|||
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
|
||||
const showOssOptions = useDialogsStore(state => state.showOssOptions);
|
||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
||||
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||
const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
|
||||
|
||||
function handleShowOptions() {
|
||||
showOssOptions();
|
||||
showOptions();
|
||||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
|
@ -110,6 +114,11 @@ export function ToolbarOssGraph({
|
|||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
onClick={resetView}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Панель содержания КС'
|
||||
icon={<IconShowSidebar value={showSidePanel} isBottom={false} size='1.25rem' />}
|
||||
onClick={toggleShowSidePanel}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Настройки отображения'
|
||||
icon={<IconSettings size='1.25rem' className='icon-primary' />}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { ListConstituents } from './list-constituents';
|
|||
export interface DlgDeleteCstProps {
|
||||
schema: IRSForm;
|
||||
selected: number[];
|
||||
afterDelete: (initialSchema: IRSForm, deleted: number[]) => void;
|
||||
afterDelete?: (initialSchema: IRSForm, deleted: number[]) => void;
|
||||
}
|
||||
|
||||
export function DlgDeleteCst() {
|
||||
|
@ -31,7 +31,7 @@ export function DlgDeleteCst() {
|
|||
|
||||
function handleSubmit() {
|
||||
const deleted = expandOut ? selected.concat(expansion) : selected;
|
||||
void cstDelete({ itemID: schema.id, data: { items: deleted } }).then(() => afterDelete(schema, deleted));
|
||||
void cstDelete({ itemID: schema.id, data: { items: deleted } }).then(() => afterDelete?.(schema, deleted));
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -54,9 +54,7 @@ export function DlgDeleteCst() {
|
|||
value={expandOut}
|
||||
onChange={value => setExpandOut(value)}
|
||||
/>
|
||||
{hasInherited ? (
|
||||
<p className='text-sm clr-text-red'>Внимание! Выбранные конституенты имеют наследников в ОСС</p>
|
||||
) : null}
|
||||
{hasInherited ? <p className='text-sm clr-text-red'>Внимание! Конституенты имеют наследников в ОСС</p> : null}
|
||||
</ModalForm>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ export function ListConstituents({ list, schema, title, prefix }: ListConstituen
|
|||
return (
|
||||
<div>
|
||||
{title ? (
|
||||
<p className='pb-1'>
|
||||
<p className='mb-1'>
|
||||
{title}: <b>{list.length}</b>
|
||||
</p>
|
||||
) : null}
|
||||
|
|
|
@ -28,6 +28,9 @@ interface PreferencesStore {
|
|||
|
||||
showExpressionControls: boolean;
|
||||
toggleShowExpressionControls: () => void;
|
||||
|
||||
showOssSidePanel: boolean;
|
||||
toggleShowOssSidePanel: () => void;
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create<PreferencesStore>()(
|
||||
|
@ -76,7 +79,10 @@ export const usePreferencesStore = create<PreferencesStore>()(
|
|||
toggleShowOSSStats: () => set(state => ({ showOSSStats: !state.showOSSStats })),
|
||||
|
||||
showExpressionControls: true,
|
||||
toggleShowExpressionControls: () => set(state => ({ showExpressionControls: !state.showExpressionControls }))
|
||||
toggleShowExpressionControls: () => set(state => ({ showExpressionControls: !state.showExpressionControls })),
|
||||
|
||||
showOssSidePanel: false,
|
||||
toggleShowOssSidePanel: () => set(state => ({ showOssSidePanel: !state.showOssSidePanel }))
|
||||
}),
|
||||
{
|
||||
version: 1,
|
||||
|
|
|
@ -148,6 +148,12 @@
|
|||
transition-duration: var(--duration-transform);
|
||||
}
|
||||
|
||||
@utility cc-animate-panel {
|
||||
transition-property: translate, opacity;
|
||||
transition-timing-function: var(--ease-bezier);
|
||||
transition-duration: var(--duration-transform);
|
||||
}
|
||||
|
||||
@utility cc-animate-position {
|
||||
transition-property: transform top left bottom right margin padding;
|
||||
transition-timing-function: var(--ease-bezier);
|
||||
|
|
Loading…
Reference in New Issue
Block a user