F: Add block stats to side panel

This commit is contained in:
Ivan 2025-07-07 15:01:25 +03:00
parent d08d3432bc
commit ae22e9b9f7
8 changed files with 53 additions and 20 deletions

View File

@ -41,7 +41,7 @@ export function HelpOssGraph() {
<IconFitImage className='inline-icon' /> Вписать в экран <IconFitImage className='inline-icon' /> Вписать в экран
</li> </li>
<li> <li>
<IconLeftOpen className='inline-icon' /> Панель связанной КС <IconLeftOpen className='inline-icon' /> Панель содержания
</li> </li>
<li> <li>
<IconSettings className='inline-icon' /> Диалог настроек <IconSettings className='inline-icon' /> Диалог настроек

View File

@ -236,7 +236,6 @@ export function OssFlow() {
showPanel ? 'translate-x-0' : 'opacity-0 translate-x-full pointer-events-none' showPanel ? 'translate-x-0' : 'opacity-0 translate-x-full pointer-events-none'
)} )}
isMounted={showPanel} isMounted={showPanel}
selectedItems={selectedItems}
/> />
</div> </div>
); );

View File

@ -0,0 +1,24 @@
import { OperationType } from '../../../../backend/types';
import { OssStats } from '../../../../components/oss-stats';
import { type IBlock, type IOperationSchema, NodeType } from '../../../../models/oss';
interface BlockStatsProps {
target: IBlock;
oss: IOperationSchema;
}
export function BlockStats({ target, oss }: BlockStatsProps) {
const contents = oss.hierarchy.expandAllOutputs([target.nodeID]);
const items = contents.map(item => oss.itemByNodeID.get(item)).filter(item => !!item);
const operations = items.filter(item => item.nodeType === NodeType.OPERATION);
const blockStats = {
count_all: contents.length,
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: operations.filter(item => !!item.result).length,
count_owned: operations.filter(item => !!item.result && item.is_owned).length,
count_block: contents.length - operations.length
};
return <OssStats stats={blockStats} className='pr-3' />;
}

View File

@ -10,19 +10,23 @@ import { useMainHeight } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { type IOssItem, NodeType } from '../../../../models/oss'; import { NodeType } from '../../../../models/oss';
import { useOssEdit } from '../../oss-edit-context';
import { BlockStats } from './block-stats';
import { ViewSchema } from './view-schema'; import { ViewSchema } from './view-schema';
interface SidePanelProps { interface SidePanelProps {
selectedItems: IOssItem[];
className?: string; className?: string;
isMounted: boolean; isMounted: boolean;
} }
export function SidePanel({ selectedItems, isMounted, className }: SidePanelProps) { export function SidePanel({ isMounted, className }: SidePanelProps) {
const { schema, isMutable, selectedItems } = useOssEdit();
const selectedOperation = const selectedOperation =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null; selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
const selectedBlock =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
const selectedSchema = selectedOperation?.result ?? null; const selectedSchema = selectedOperation?.result ?? null;
const debouncedMounted = useDebounce(isMounted, PARAMETER.moveDuration); const debouncedMounted = useDebounce(isMounted, PARAMETER.moveDuration);
@ -52,21 +56,23 @@ export function SidePanel({ selectedItems, isMounted, className }: SidePanelProp
'mt-0 mb-1', 'mt-0 mb-1',
'font-medium text-sm select-none self-center', 'font-medium text-sm select-none self-center',
'transition-transform', 'transition-transform',
selectedSchema && 'translate-x-16' selectedSchema && 'translate-x-20'
)} )}
> >
Содержание КС Содержание
</div> </div>
{!selectedOperation ? ( {!selectedOperation && !selectedBlock ? (
<div className='text-center text-sm cc-fade-in'>Выделите операцию для просмотра</div> <div className='text-center text-sm cc-fade-in'>Выделите операцию или блок для просмотра</div>
) : !selectedSchema ? ( ) : null}
{selectedOperation && !selectedSchema ? (
<div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div> <div className='text-center text-sm cc-fade-in'>Отсутствует концептуальная схема для выбранной операции</div>
) : debouncedMounted ? ( ) : selectedOperation && selectedSchema && debouncedMounted ? (
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<ViewSchema schemaID={selectedSchema} /> <ViewSchema schemaID={selectedSchema} isMutable={isMutable && selectedOperation.is_owned} />
</Suspense> </Suspense>
) : null} ) : null}
{selectedBlock ? <BlockStats target={selectedBlock} oss={schema} /> : null}
</aside> </aside>
); );
} }

View File

@ -16,6 +16,7 @@ import { type RO } from '@/utils/meta';
interface ToolbarConstituentsProps { interface ToolbarConstituentsProps {
schema: IRSForm; schema: IRSForm;
isMutable: boolean;
activeCst: IConstituenta | null; activeCst: IConstituenta | null;
setActive: (cstID: number) => void; setActive: (cstID: number) => void;
resetActive: () => void; resetActive: () => void;
@ -27,6 +28,7 @@ export function ToolbarConstituents({
activeCst, activeCst,
setActive, setActive,
resetActive, resetActive,
isMutable,
className className
}: ToolbarConstituentsProps) { }: ToolbarConstituentsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
@ -162,33 +164,33 @@ export function ToolbarConstituents({
title='Создать конституенту' title='Создать конституенту'
icon={<IconNewItem size='1rem' className='icon-green' />} icon={<IconNewItem size='1rem' className='icon-green' />}
onClick={createCst} onClick={createCst}
disabled={isProcessing} disabled={!isMutable || isProcessing}
/> />
<MiniButton <MiniButton
title='Клонировать конституенту' title='Клонировать конституенту'
icon={<IconClone size='1rem' className='icon-green' />} icon={<IconClone size='1rem' className='icon-green' />}
onClick={cloneCst} onClick={cloneCst}
disabled={!activeCst || isProcessing} disabled={!isMutable || !activeCst || isProcessing}
/> />
<MiniButton <MiniButton
title='Удалить выделенную конституенту' title='Удалить выделенную конституенту'
onClick={promptDeleteCst} onClick={promptDeleteCst}
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!activeCst || isProcessing || activeCst?.is_inherited} disabled={!isMutable || !activeCst || isProcessing || activeCst?.is_inherited}
/> />
<MiniButton <MiniButton
title='Переместить вверх' title='Переместить вверх'
icon={<IconMoveUp size='1rem' className='icon-primary' />} icon={<IconMoveUp size='1rem' className='icon-primary' />}
onClick={moveUp} onClick={moveUp}
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch} disabled={!isMutable || !activeCst || isProcessing || schema.items.length < 2 || hasSearch}
/> />
<MiniButton <MiniButton
title='Переместить вниз' title='Переместить вниз'
icon={<IconMoveDown size='1rem' className='icon-primary' />} icon={<IconMoveDown size='1rem' className='icon-primary' />}
onClick={moveDown} onClick={moveDown}
disabled={!activeCst || isProcessing || schema.items.length < 2 || hasSearch} disabled={!isMutable || !activeCst || isProcessing || schema.items.length < 2 || hasSearch}
/> />
</div> </div>
); );

View File

@ -10,9 +10,10 @@ import { ToolbarConstituents } from './toolbar-constituents';
interface ViewSchemaProps { interface ViewSchemaProps {
schemaID: number; schemaID: number;
isMutable: boolean;
} }
export function ViewSchema({ schemaID }: ViewSchemaProps) { export function ViewSchema({ schemaID, isMutable }: ViewSchemaProps) {
const { schema } = useRSFormSuspense({ itemID: schemaID }); const { schema } = useRSFormSuspense({ itemID: schemaID });
const [activeID, setActiveID] = useState<number | null>(null); const [activeID, setActiveID] = useState<number | null>(null);
const activeCst = activeID ? schema.cstByID.get(activeID) ?? null : null; const activeCst = activeID ? schema.cstByID.get(activeID) ?? null : null;
@ -25,6 +26,7 @@ export function ViewSchema({ schemaID }: ViewSchemaProps) {
className='absolute -top-7 left-1' className='absolute -top-7 left-1'
schema={schema} schema={schema}
activeCst={activeCst} activeCst={activeCst}
isMutable={isMutable}
setActive={setActiveID} setActive={setActiveID}
resetActive={() => setActiveID(null)} resetActive={() => setActiveID(null)}
/> />

View File

@ -115,7 +115,7 @@ export function ToolbarOssGraph({
onClick={resetView} onClick={resetView}
/> />
<MiniButton <MiniButton
title='Панель содержания КС' title='Панель содержания'
icon={<IconShowSidebar value={showSidePanel} isBottom={false} size='1.25rem' />} icon={<IconShowSidebar value={showSidePanel} isBottom={false} size='1.25rem' />}
onClick={toggleShowSidePanel} onClick={toggleShowSidePanel}
/> />

View File

@ -98,7 +98,7 @@ export function TableSideConstituents({
noDataComponent={ noDataComponent={
<NoData className='min-h-20'> <NoData className='min-h-20'>
<p>Список конституент пуст</p> <p>Список конституент пуст</p>
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра или создайте конституенту</p>
</NoData> </NoData>
} }
onRowClicked={onActivate ? cst => onActivate(cst) : undefined} onRowClicked={onActivate ? cst => onActivate(cst) : undefined}