F: Improve oss UI
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run
Frontend CI / notify-failure (push) Blocked by required conditions

This commit is contained in:
Ivan 2025-04-24 13:16:50 +03:00
parent c25f22f340
commit 8cf3a0efe1
11 changed files with 162 additions and 71 deletions

View File

@ -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:

View File

@ -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';

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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

View File

@ -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}

View File

@ -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) {

View File

@ -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>

View 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;
}

View File

@ -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 }),

View File

@ -325,6 +325,7 @@
.selected & {
color: var(--color-foreground);
border-color: var(--color-graph-selected);
border-style: solid;
}
&:hover {