mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-25 20:40:36 +03:00
F: Improve oss UI
This commit is contained in:
parent
c25f22f340
commit
8cf3a0efe1
|
@ -123,6 +123,11 @@ const DlgEditBlock = React.lazy(() =>
|
|||
default: module.DlgEditBlock
|
||||
}))
|
||||
);
|
||||
const DlgOssSettings = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-oss-settings').then(module => ({
|
||||
default: module.DlgOssSettings
|
||||
}))
|
||||
);
|
||||
|
||||
export const GlobalDialogs = () => {
|
||||
const active = useDialogsStore(state => state.active);
|
||||
|
@ -155,6 +160,8 @@ export const GlobalDialogs = () => {
|
|||
return <DlgEditWordForms />;
|
||||
case DialogType.INLINE_SYNTHESIS:
|
||||
return <DlgInlineSynthesis />;
|
||||
case DialogType.OSS_SETTINGS:
|
||||
return <DlgOssSettings />;
|
||||
case DialogType.SHOW_AST:
|
||||
return <DlgShowAST />;
|
||||
case DialogType.SHOW_TYPE_GRAPH:
|
||||
|
|
|
@ -16,7 +16,7 @@ export { FiEdit as IconEdit2 } from 'react-icons/fi';
|
|||
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
|
||||
export { BiDownload as IconDownload } from 'react-icons/bi';
|
||||
export { BiUpload as IconUpload } from 'react-icons/bi';
|
||||
export { BiCog as IconSettings } from 'react-icons/bi';
|
||||
export { LuSettings as IconSettings } from 'react-icons/lu';
|
||||
export { TbEye as IconShow } from 'react-icons/tb';
|
||||
export { TbEyeX as IconHide } from 'react-icons/tb';
|
||||
export { BiShareAlt as IconShare } from 'react-icons/bi';
|
||||
|
@ -141,6 +141,7 @@ export { GrConnect as IconConnect } from 'react-icons/gr';
|
|||
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
||||
|
||||
// ======== Graph UI =======
|
||||
export { LuLayoutDashboard as IconFixLayout } from 'react-icons/lu';
|
||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||
|
|
|
@ -14,10 +14,13 @@ export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onCha
|
|||
disabled?: boolean;
|
||||
|
||||
/** Current value - `true` or `false`. */
|
||||
value?: boolean;
|
||||
value: boolean;
|
||||
|
||||
/** Callback to set the `value`. */
|
||||
onChange?: (newValue: boolean) => void;
|
||||
|
||||
/** Custom icon to display next instead of checkbox. */
|
||||
customIcon?: (checked?: boolean) => React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,6 +34,7 @@ export function Checkbox({
|
|||
hideTitle,
|
||||
className,
|
||||
value,
|
||||
customIcon,
|
||||
onChange,
|
||||
...restProps
|
||||
}: CheckboxProps) {
|
||||
|
@ -63,15 +67,19 @@ export function Checkbox({
|
|||
disabled={disabled}
|
||||
{...restProps}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'border rounded-sm',
|
||||
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{value ? <CheckboxChecked /> : null}
|
||||
</div>
|
||||
{customIcon ? (
|
||||
customIcon(value)
|
||||
) : (
|
||||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4', //
|
||||
'border rounded-sm',
|
||||
value === false ? 'bg-background text-foreground' : 'bg-primary text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{value ? <CheckboxChecked /> : null}
|
||||
</div>
|
||||
)}
|
||||
{label ? <span className={clsx('text-start text-sm whitespace-nowrap select-text', cursor)}>{label}</span> : null}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
IconAnimation,
|
||||
IconAnimationOff,
|
||||
IconCoordinates,
|
||||
IconGrid,
|
||||
IconLineStraight,
|
||||
IconLineWave
|
||||
} from '@/components/icons';
|
||||
import { Checkbox } from '@/components/input';
|
||||
import { ModalView } from '@/components/modal';
|
||||
|
||||
import { useOSSGraphStore } from '../stores/oss-graph';
|
||||
|
||||
const ICON_SIZE = '1.5rem';
|
||||
|
||||
export function DlgOssSettings() {
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
const toggleShowGrid = useOSSGraphStore(state => state.toggleShowGrid);
|
||||
const toggleShowCoordinates = useOSSGraphStore(state => state.toggleShowCoordinates);
|
||||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||
|
||||
return (
|
||||
<ModalView header='Настройки отображения' className='cc-column justify-between px-6 pb-3 w-100'>
|
||||
<Checkbox
|
||||
value={showCoordinates}
|
||||
onChange={toggleShowCoordinates}
|
||||
aria-label='Переключатель отображения координат'
|
||||
label={`Координаты узлов: ${showCoordinates ? 'Вкл' : 'Выкл'}`}
|
||||
customIcon={checked => <IconCoordinates size={ICON_SIZE} className={checked ? 'icon-green' : 'icon-primary'} />}
|
||||
/>
|
||||
<Checkbox
|
||||
value={showGrid}
|
||||
onChange={toggleShowGrid}
|
||||
aria-label='Переключатель отображения сетки'
|
||||
label={`Отображение сетки: ${showGrid ? 'Вкл' : 'Выкл'}`}
|
||||
customIcon={checked => <IconGrid size={ICON_SIZE} className={checked ? 'icon-green' : 'icon-primary'} />}
|
||||
/>
|
||||
<Checkbox
|
||||
value={edgeAnimate}
|
||||
onChange={toggleEdgeAnimate}
|
||||
aria-label='Переключатель анимации связей'
|
||||
label={`Анимация связей: ${edgeAnimate ? 'Вкл' : 'Выкл'}`}
|
||||
customIcon={checked =>
|
||||
checked ? (
|
||||
<IconAnimation size={ICON_SIZE} className='icon-primary' />
|
||||
) : (
|
||||
<IconAnimationOff size={ICON_SIZE} className='icon-primary' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
value={edgeStraight}
|
||||
onChange={toggleEdgeStraight}
|
||||
aria-label='Переключатель формы связей'
|
||||
label={`Связи: ${edgeStraight ? 'Прямые' : 'Безье'}`}
|
||||
customIcon={checked =>
|
||||
checked ? (
|
||||
<IconLineStraight size={ICON_SIZE} className='icon-primary' />
|
||||
) : (
|
||||
<IconLineWave size={ICON_SIZE} className='icon-primary' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ModalView>
|
||||
);
|
||||
}
|
|
@ -46,7 +46,7 @@ export function BlockNode(node: BlockInternalNode) {
|
|||
'cc-node-block h-full w-full',
|
||||
isDragging && isParent && dropTarget !== node.data.block.id && 'border-destructive',
|
||||
((isParent && !isDragging) || dropTarget === node.data.block.id) && 'border-primary',
|
||||
isChild && 'border-accent-orange50'
|
||||
isChild && 'border-accent-orange'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -40,7 +40,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
className={cn(
|
||||
'cc-node-operation h-[40px] w-[150px]',
|
||||
'relative flex items-center justify-center p-[2px]',
|
||||
isChild && 'border-accent-orange50!'
|
||||
isChild && 'border-accent-orange'
|
||||
)}
|
||||
data-tooltip-id={globalIDs.operation_tooltip}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
|
|||
import { useMoveItems } from '@/features/oss/backend/use-move-items';
|
||||
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||
|
||||
import { useThrottleCallback } from '@/hooks/use-throttle-callback';
|
||||
import { useMainHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -42,6 +43,8 @@ const ZOOM_MIN = 0.5;
|
|||
const Z_BLOCK = 1;
|
||||
const Z_SCHEMA = 10;
|
||||
|
||||
const DRAG_THROTTLE_DELAY = 50; // ms
|
||||
|
||||
export const VIEW_PADDING = 0.2;
|
||||
|
||||
export function OssFlow() {
|
||||
|
@ -269,7 +272,7 @@ export function OssFlow() {
|
|||
|
||||
function determineDropTarget(event: React.MouseEvent): number | null {
|
||||
const mousePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||
const blocks = getIntersectingNodes({
|
||||
let blocks = getIntersectingNodes({
|
||||
x: mousePosition.x,
|
||||
y: mousePosition.y,
|
||||
width: 1,
|
||||
|
@ -283,6 +286,12 @@ export function OssFlow() {
|
|||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const successors = schema.hierarchy.expandAllOutputs([...selected]).filter(id => id < 0);
|
||||
blocks = blocks.filter(block => !successors.includes(-block.id));
|
||||
if (blocks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (blocks.length === 1) {
|
||||
return blocks[0].id;
|
||||
}
|
||||
|
@ -317,13 +326,13 @@ export function OssFlow() {
|
|||
setIsContextMenuOpen(false);
|
||||
}
|
||||
|
||||
function handleDrag(event: React.MouseEvent) {
|
||||
const handleDrag = useThrottleCallback((event: React.MouseEvent) => {
|
||||
if (containMovement) {
|
||||
return;
|
||||
}
|
||||
setIsDragging(true);
|
||||
setDropTarget(determineDropTarget(event));
|
||||
}
|
||||
}, DRAG_THROTTLE_DELAY);
|
||||
|
||||
function handleDragStop(event: React.MouseEvent, target: Node) {
|
||||
if (containMovement) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
|
@ -9,20 +10,16 @@ import { useUpdateLayout } from '@/features/oss/backend/use-update-layout';
|
|||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import {
|
||||
IconAnimation,
|
||||
IconAnimationOff,
|
||||
IconConceptBlock,
|
||||
IconCoordinates,
|
||||
IconDestroy,
|
||||
IconEdit2,
|
||||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconFixLayout,
|
||||
IconNewItem,
|
||||
IconReset,
|
||||
IconSave
|
||||
IconSave,
|
||||
IconSettings
|
||||
} from '@/components/icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
|
@ -32,7 +29,6 @@ import { prepareTooltip } from '@/utils/utils';
|
|||
|
||||
import { OperationType } from '../../../backend/types';
|
||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||
import { useOssEdit } from '../oss-edit-context';
|
||||
|
||||
import { VIEW_PADDING } from './oss-flow';
|
||||
|
@ -60,20 +56,12 @@ export function ToolbarOssGraph({
|
|||
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null;
|
||||
const getLayout = useGetLayout();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
const toggleShowGrid = useOSSGraphStore(state => state.toggleShowGrid);
|
||||
const toggleShowCoordinates = useOSSGraphStore(state => state.toggleShowCoordinates);
|
||||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||
|
||||
const { updateLayout } = useUpdateLayout();
|
||||
const { executeOperation } = useExecuteOperation();
|
||||
|
||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||
const showOssOptions = useDialogsStore(state => state.showOssOptions);
|
||||
|
||||
const readyForSynthesis = (() => {
|
||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||
|
@ -100,6 +88,15 @@ export function ToolbarOssGraph({
|
|||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||
}
|
||||
|
||||
function handleFixLayout() {
|
||||
// TODO: implement layout algorithm
|
||||
toast.info('Еще не реализовано');
|
||||
}
|
||||
|
||||
function handleShowOptions() {
|
||||
showOssOptions();
|
||||
}
|
||||
|
||||
function handleSavePositions() {
|
||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||
}
|
||||
|
@ -152,46 +149,15 @@ export function ToolbarOssGraph({
|
|||
onClick={handleFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||
aria-label='Переключатель отображения сетки'
|
||||
icon={
|
||||
showGrid ? (
|
||||
<IconGrid size='1.25rem' className='icon-green' />
|
||||
) : (
|
||||
<IconGrid size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleShowGrid}
|
||||
title='Исправить позиции узлов'
|
||||
icon={<IconFixLayout size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleFixLayout}
|
||||
disabled={selected.length > 1 || selected[0] > 0}
|
||||
/>
|
||||
<MiniButton
|
||||
title={edgeStraight ? 'Связи: прямые' : 'Связи: безье'}
|
||||
aria-label='Переключатель формы связей'
|
||||
icon={
|
||||
edgeStraight ? (
|
||||
<IconLineStraight size='1.25rem' className='icon-primary' />
|
||||
) : (
|
||||
<IconLineWave size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleEdgeStraight}
|
||||
/>
|
||||
<MiniButton
|
||||
title={edgeAnimate ? 'Анимация: вкл' : 'Анимация: выкл'}
|
||||
aria-label='Переключатель анимации связей'
|
||||
icon={
|
||||
edgeAnimate ? (
|
||||
<IconAnimation size='1.25rem' className='icon-primary' />
|
||||
) : (
|
||||
<IconAnimationOff size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleEdgeAnimate}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showCoordinates ? 'Координаты: вкл' : 'Координаты: выкл'}
|
||||
aria-label='Переключатель видимости координат (для отладки)'
|
||||
icon={<IconCoordinates size='1.25rem' className={showCoordinates ? 'icon-green' : 'icon-primary'} />}
|
||||
onClick={toggleShowCoordinates}
|
||||
title='Настройки отображения'
|
||||
icon={<IconSettings size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleShowOptions}
|
||||
/>
|
||||
<BadgeHelp topic={HelpTopic.UI_OSS_GRAPH} contentClass='sm:max-w-160' offset={4} />
|
||||
</div>
|
||||
|
|
24
rsconcept/frontend/src/hooks/use-throttle-callback.ts
Normal file
24
rsconcept/frontend/src/hooks/use-throttle-callback.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
/** Throttles a callback to only run once per delay. */
|
||||
export function useThrottleCallback<Callback extends (...args: never[]) => void>(
|
||||
callback: Callback,
|
||||
delay: number
|
||||
): Callback {
|
||||
const lastCalled = useRef(0);
|
||||
|
||||
const throttled = useCallback(
|
||||
(...args: Parameters<Callback>) => {
|
||||
const now = Date.now();
|
||||
if (now - lastCalled.current >= delay) {
|
||||
lastCalled.current = now;
|
||||
callback(...args);
|
||||
}
|
||||
},
|
||||
[callback, delay]
|
||||
);
|
||||
|
||||
return throttled as Callback;
|
||||
}
|
|
@ -44,6 +44,7 @@ export const DialogType = {
|
|||
DELETE_OPERATION: 10,
|
||||
CHANGE_INPUT_SCHEMA: 11,
|
||||
RELOCATE_CONSTITUENTS: 12,
|
||||
OSS_SETTINGS: 26,
|
||||
|
||||
CLONE_LIBRARY_ITEM: 13,
|
||||
UPLOAD_RSFORM: 14,
|
||||
|
@ -91,6 +92,7 @@ interface DialogsStore {
|
|||
showCreateVersion: (props: DlgCreateVersionProps) => void;
|
||||
showDeleteOperation: (props: DlgDeleteOperationProps) => void;
|
||||
showGraphParams: () => void;
|
||||
showOssOptions: () => void;
|
||||
showRelocateConstituents: (props: DlgRelocateConstituentsProps) => void;
|
||||
showRenameCst: (props: DlgRenameCstProps) => void;
|
||||
showQR: (props: DlgShowQRProps) => void;
|
||||
|
@ -128,6 +130,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
|
|||
showCreateVersion: props => set({ active: DialogType.CREATE_VERSION, props: props }),
|
||||
showDeleteOperation: props => set({ active: DialogType.DELETE_OPERATION, props: props }),
|
||||
showGraphParams: () => set({ active: DialogType.GRAPH_PARAMETERS, props: null }),
|
||||
showOssOptions: () => set({ active: DialogType.OSS_SETTINGS, props: null }),
|
||||
showRelocateConstituents: props => set({ active: DialogType.RELOCATE_CONSTITUENTS, props: props }),
|
||||
showRenameCst: props => set({ active: DialogType.RENAME_CONSTITUENTA, props: props }),
|
||||
showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }),
|
||||
|
|
|
@ -325,6 +325,7 @@
|
|||
.selected & {
|
||||
color: var(--color-foreground);
|
||||
border-color: var(--color-graph-selected);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
Loading…
Reference in New Issue
Block a user