F: Improve OSS UI

This commit is contained in:
Ivan 2024-07-26 00:34:08 +03:00
parent 3d80ac1292
commit b7e6b24e44
14 changed files with 174 additions and 22 deletions

View File

@ -0,0 +1,33 @@
import Tooltip from '@/components/ui/Tooltip';
import { IOperation } from '@/models/oss';
import { labelOperationType } from '@/utils/labels';
interface TooltipOperationProps {
data: IOperation;
anchor: string;
}
function TooltipOperation({ data, anchor }: TooltipOperationProps) {
return (
<Tooltip layer='z-modalTooltip' anchorSelect={anchor} className='max-w-[30rem] dense'>
<h2>Операция {data.alias}</h2>
<p>
<b>Тип:</b> {labelOperationType(data.operation_type)}
</p>
{data.title ? (
<p>
<b>Название: </b>
{data.title}
</p>
) : null}
{data.comment ? (
<p>
<b>Комментарий: </b>
{data.comment}
</p>
) : null}
</Tooltip>
);
}
export default TooltipOperation;

View File

@ -16,12 +16,13 @@ interface PickSchemaProps {
rows?: number; rows?: number;
value?: LibraryItemID; value?: LibraryItemID;
baseFilter?: (target: ILibraryItem) => boolean;
onSelectValue: (newValue: LibraryItemID) => void; onSelectValue: (newValue: LibraryItemID) => void;
} }
const columnHelper = createColumnHelper<ILibraryItem>(); const columnHelper = createColumnHelper<ILibraryItem>();
function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue }: PickSchemaProps) { function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue, baseFilter }: PickSchemaProps) {
const intl = useIntl(); const intl = useIntl();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
@ -38,8 +39,13 @@ function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue }:
}, [filterText]); }, [filterText]);
useLayoutEffect(() => { useLayoutEffect(() => {
setItems(library.applyFilter(filter)); const filtered = library.applyFilter(filter);
}, [library, filter, filter.query]); if (baseFilter) {
setItems(filtered.filter(baseFilter));
} else {
setItems(filtered);
}
}, [library, filter, filter.query, baseFilter]);
const columns = useMemo( const columns = useMemo(
() => [ () => [

View File

@ -77,6 +77,7 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
() => ( () => (
<TabPanel> <TabPanel>
<TabInputOperation <TabInputOperation
oss={oss}
alias={alias} alias={alias}
setAlias={setAlias} setAlias={setAlias}
comment={comment} comment={comment}
@ -90,7 +91,7 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
/> />
</TabPanel> </TabPanel>
), ),
[alias, comment, title, attachedID, syncText] [alias, comment, title, attachedID, syncText, oss]
); );
const synthesisPanel = useMemo( const synthesisPanel = useMemo(

View File

@ -1,3 +1,7 @@
'use client';
import { useCallback } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
@ -7,10 +11,12 @@ import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationSchema } from '@/models/oss';
import { limits, patterns } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
interface TabInputOperationProps { interface TabInputOperationProps {
oss: IOperationSchema;
alias: string; alias: string;
setAlias: React.Dispatch<React.SetStateAction<string>>; setAlias: React.Dispatch<React.SetStateAction<string>>;
title: string; title: string;
@ -24,6 +30,7 @@ interface TabInputOperationProps {
} }
function TabInputOperation({ function TabInputOperation({
oss,
alias, alias,
setAlias, setAlias,
title, title,
@ -35,6 +42,8 @@ function TabInputOperation({
syncText, syncText,
setSyncText setSyncText
}: TabInputOperationProps) { }: TabInputOperationProps) {
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
return ( return (
<AnimateFade className='cc-column'> <AnimateFade className='cc-column'>
<TextInput <TextInput
@ -87,7 +96,12 @@ function TabInputOperation({
/> />
</div> </div>
<PickSchema value={attachedID} onSelectValue={setAttachedID} rows={8} /> <PickSchema
value={attachedID} // prettier: split-line
onSelectValue={setAttachedID}
rows={8}
baseFilter={baseFilter}
/>
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -3,7 +3,15 @@
*/ */
import { Graph } from './Graph'; import { Graph } from './Graph';
import { IOperation, IOperationSchema, IOperationSchemaData, OperationID } from './oss'; import { LibraryItemID } from './library';
import {
IOperation,
IOperationSchema,
IOperationSchemaData,
IOperationSchemaStats,
OperationID,
OperationType
} from './oss';
/** /**
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}. * Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
@ -13,6 +21,7 @@ export class OssLoader {
private oss: IOperationSchemaData; private oss: IOperationSchemaData;
private graph: Graph = new Graph(); private graph: Graph = new Graph();
private operationByID: Map<OperationID, IOperation> = new Map(); private operationByID: Map<OperationID, IOperation> = new Map();
private schemas: LibraryItemID[] = [];
constructor(input: IOperationSchemaData) { constructor(input: IOperationSchemaData) {
this.oss = input; this.oss = input;
@ -22,9 +31,12 @@ export class OssLoader {
const result = this.oss as IOperationSchema; const result = this.oss as IOperationSchema;
this.prepareLookups(); this.prepareLookups();
this.createGraph(); this.createGraph();
this.extractSchemas();
result.operationByID = this.operationByID; result.operationByID = this.operationByID;
result.graph = this.graph; result.graph = this.graph;
result.schemas = this.schemas;
result.stats = this.calculateStats();
return result; return result;
} }
@ -38,4 +50,18 @@ export class OssLoader {
private createGraph() { private createGraph() {
this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation)); this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation));
} }
private extractSchemas() {
this.schemas = this.oss.items.map(operation => operation.result as LibraryItemID).filter(item => item !== null);
}
private calculateStats(): IOperationSchemaStats {
const items = this.oss.items;
return {
count_operations: items.length,
count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemas.length
};
}
} }

View File

@ -102,6 +102,16 @@ export interface ICstSubstituteEx extends ICstSubstitute {
substitution_term: string; substitution_term: string;
} }
/**
* Represents {@link IOperationSchema} statistics.
*/
export interface IOperationSchemaStats {
count_operations: number;
count_inputs: number;
count_synthesis: number;
count_schemas: number;
}
/** /**
* Represents backend data for {@link IOperationSchema}. * Represents backend data for {@link IOperationSchema}.
*/ */
@ -116,6 +126,8 @@ export interface IOperationSchemaData extends ILibraryItemData {
*/ */
export interface IOperationSchema extends IOperationSchemaData { export interface IOperationSchema extends IOperationSchemaData {
graph: Graph; graph: Graph;
schemas: LibraryItemID[];
stats: IOperationSchemaStats;
operationByID: Map<OperationID, IOperation>; operationByID: Map<OperationID, IOperation>;
} }

View File

@ -12,6 +12,7 @@ import { globals } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import FormOSS from './FormOSS'; import FormOSS from './FormOSS';
import OssStats from './OssStats';
interface EditorOssCardProps { interface EditorOssCardProps {
isModified: boolean; isModified: boolean;
@ -50,11 +51,13 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
onDestroy={onDestroy} onDestroy={onDestroy}
controller={controller} controller={controller}
/> />
<AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row')}> <AnimateFade onKeyDown={handleInput} className={clsx('sm:w-fit mx-auto', 'flex flex-col sm:flex-row px-6')}>
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} /> <FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} /> <EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
</FlexColumn> </FlexColumn>
<OssStats stats={schema?.stats} />
</AnimateFade> </AnimateFade>
</> </>
); );

View File

@ -0,0 +1,28 @@
import Divider from '@/components/ui/Divider';
import LabeledValue from '@/components/ui/LabeledValue';
import { IOperationSchemaStats } from '@/models/oss';
interface OssStatsProps {
stats?: IOperationSchemaStats;
}
function OssStats({ stats }: OssStatsProps) {
if (!stats) {
return null;
}
return (
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'>
<Divider margins='my-2' className='sm:hidden' />
<LabeledValue id='count_all' label='Всего операций' text={stats.count_operations} />
<LabeledValue id='count_inputs' label='Загрузка' text={stats.count_inputs} />
<LabeledValue id='count_synthesis' label='Синтез' text={stats.count_synthesis} />
<Divider margins='my-2' />
<LabeledValue id='count_schemas' label='Прикрепленные схемы' text={stats.count_schemas} />
</div>
);
}
export default OssStats;

View File

@ -1,9 +1,11 @@
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { IconRSForm } from '@/components/Icons'; import { IconRSForm } from '@/components/Icons';
import TooltipOperation from '@/components/info/TooltipOperation';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useOSS } from '@/context/OssContext'; import { IOperation } from '@/models/oss';
import { prefixes } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
@ -11,14 +13,14 @@ interface InputNodeProps {
id: string; id: string;
data: { data: {
label: string; label: string;
operation: IOperation;
}; };
} }
function InputNode({ id, data }: InputNodeProps) { function InputNode({ id, data }: InputNodeProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const model = useOSS();
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result; const hasFile = !!data.operation.result;
const handleOpenSchema = () => { const handleOpenSchema = () => {
controller.openOperationSchema(Number(id)); controller.openOperationSchema(Number(id));
@ -39,7 +41,12 @@ function InputNode({ id, data }: InputNodeProps) {
disabled={!hasFile} disabled={!hasFile}
/> />
</Overlay> </Overlay>
<div className='flex-grow text-center text-sm'>{data.label}</div> <div id={`${prefixes.operation_list}${id}`} className='flex-grow text-center text-sm'>
{data.label}
{controller.showTooltip ? (
<TooltipOperation anchor={`#${prefixes.operation_list}${id}`} data={data.operation} />
) : null}
</div>
</> </>
); );
} }

View File

@ -1,23 +1,25 @@
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { IconRSForm } from '@/components/Icons'; import { IconRSForm } from '@/components/Icons';
import TooltipOperation from '@/components/info/TooltipOperation';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useOSS } from '@/context/OssContext'; import { IOperation } from '@/models/oss';
import { prefixes } from '@/utils/constants';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface OperationNodeProps { interface OperationNodeProps {
id: string; id: string;
data: { data: {
label: string; label: string;
operation: IOperation;
}; };
} }
function OperationNode({ id, data }: OperationNodeProps) { function OperationNode({ id, data }: OperationNodeProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const model = useOSS();
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result; const hasFile = !!data.operation.result;
const handleOpenSchema = () => { const handleOpenSchema = () => {
controller.openOperationSchema(Number(id)); controller.openOperationSchema(Number(id));
@ -38,7 +40,11 @@ function OperationNode({ id, data }: OperationNodeProps) {
disabled={!hasFile} disabled={!hasFile}
/> />
</Overlay> </Overlay>
<div className='flex-grow text-center text-sm'>{data.label}</div>
<div id={`${prefixes.operation_list}${id}`} className='flex-grow text-center text-sm'>
{data.label}
<TooltipOperation anchor={`#${prefixes.operation_list}${id}`} data={data.operation} />
</div>
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} /> <Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />
<Handle type='target' position={Position.Top} id='right' style={{ right: 40, left: 'auto' }} /> <Handle type='target' position={Position.Top} id='right' style={{ right: 40, left: 'auto' }} />

View File

@ -50,7 +50,6 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
const onSelectionChange = useCallback( const onSelectionChange = useCallback(
({ nodes }: { nodes: Node[] }) => { ({ nodes }: { nodes: Node[] }) => {
controller.setSelected(nodes.map(node => Number(node.id))); controller.setSelected(nodes.map(node => Number(node.id)));
console.log(nodes);
}, },
[controller] [controller]
); );
@ -67,7 +66,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
setNodes( setNodes(
model.schema.items.map(operation => ({ model.schema.items.map(operation => ({
id: String(operation.id), id: String(operation.id),
data: { label: operation.alias }, data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y }, position: { x: operation.position_x, y: operation.position_y },
type: operation.operation_type.toString() type: operation.operation_type.toString()
})) }))
@ -116,7 +115,6 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
const handleCreateOperation = useCallback(() => { const handleCreateOperation = useCallback(() => {
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
console.log(center);
controller.promptCreateOperation(center.x, center.y, getPositions()); controller.promptCreateOperation(center.x, center.y, getPositions());
}, [controller, getPositions, flow]); }, [controller, getPositions, flow]);
@ -168,8 +166,17 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
}); });
}, [colors, nodes]); }, [colors, nodes]);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
controller.setShowTooltip(prev => !prev);
// setShowContextMenu(true);
},
[controller]
);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
// Hotkeys implementation
if (controller.isProcessing) { if (controller.isProcessing) {
return; return;
} }
@ -217,11 +224,12 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr
nodesConnectable={false} nodesConnectable={false}
snapToGrid={true} snapToGrid={true}
snapGrid={[10, 10]} snapGrid={[10, 10]}
onContextMenu={handleContextMenu}
> >
{showGrid ? <Background gap={10} /> : null} {showGrid ? <Background gap={10} /> : null}
</ReactFlow> </ReactFlow>
), ),
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes, showGrid] [nodes, edges, proOptions, handleNodesChange, handleContextMenu, onEdgesChange, OssNodeTypes, showGrid]
); );
return ( return (

View File

@ -26,6 +26,9 @@ export interface IOssEditContext {
isMutable: boolean; isMutable: boolean;
isProcessing: boolean; isProcessing: boolean;
showTooltip: boolean;
setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>;
setOwner: (newOwner: UserID) => void; setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void; setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void; promptEditors: () => void;
@ -71,6 +74,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[accessLevel, model.schema?.read_only] [accessLevel, model.schema?.read_only]
); );
const [showTooltip, setShowTooltip] = useState(true);
const [showEditEditors, setShowEditEditors] = useState(false); const [showEditEditors, setShowEditEditors] = useState(false);
const [showEditLocation, setShowEditLocation] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false);
@ -211,6 +216,9 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
schema: model.schema, schema: model.schema,
selected, selected,
showTooltip,
setShowTooltip,
isMutable, isMutable,
isProcessing: model.processing, isProcessing: model.processing,

View File

@ -180,7 +180,6 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
}, [graphRef]); }, [graphRef]);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
// Hotkeys implementation
if (controller.isProcessing) { if (controller.isProcessing) {
return; return;
} }

View File

@ -148,6 +148,7 @@ export const prefixes = {
cst_source_list: 'cst_source_list_', cst_source_list: 'cst_source_list_',
cst_delete_list: 'cst_delete_list_', cst_delete_list: 'cst_delete_list_',
cst_dependant_list: 'cst_dependant_list_', cst_dependant_list: 'cst_dependant_list_',
operation_list: 'operation_list_',
csttype_list: 'csttype_', csttype_list: 'csttype_',
policy_list: 'policy_list_', policy_list: 'policy_list_',
library_filters_list: 'library_filters_list_', library_filters_list: 'library_filters_list_',