From a3241ecff74ceaeef49801c894bd2c4f517181d0 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:15:57 +0300 Subject: [PATCH] F: Improve oss UI --- rsconcept/frontend/src/app/global-dialogs.tsx | 7 ++ rsconcept/frontend/src/components/icons.tsx | 3 +- .../src/components/input/checkbox.tsx | 28 ++++--- .../frontend/src/features/oss/backend/api.ts | 2 +- .../features/oss/dialogs/dlg-edit-block.tsx | 2 +- .../features/oss/dialogs/dlg-oss-settings.tsx | 72 ++++++++++++++++++ .../context-menu/context-menu.tsx | 1 + .../context-menu/menu-block.tsx | 4 +- .../editor-oss-graph/graph/block-node.tsx | 9 ++- .../editor-oss-graph/graph/node-core.tsx | 2 +- .../oss-page/editor-oss-graph/oss-flow.tsx | 36 +++++++-- .../editor-oss-graph/toolbar-oss-graph.tsx | 76 +++++-------------- .../rsform/dialogs/dlg-show-ast/ast-flow.tsx | 1 + .../dlg-show-type-graph/mgraph-flow.tsx | 1 + .../src/hooks/use-throttle-callback.ts | 24 ++++++ rsconcept/frontend/src/stores/dialogs.ts | 3 + rsconcept/frontend/src/styling/components.css | 1 + rsconcept/frontend/src/styling/reactflow.css | 2 +- rsconcept/frontend/src/utils/labels.ts | 1 + 19 files changed, 193 insertions(+), 82 deletions(-) create mode 100644 rsconcept/frontend/src/features/oss/dialogs/dlg-oss-settings.tsx create mode 100644 rsconcept/frontend/src/hooks/use-throttle-callback.ts diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx index f0c79d34..911833a5 100644 --- a/rsconcept/frontend/src/app/global-dialogs.tsx +++ b/rsconcept/frontend/src/app/global-dialogs.tsx @@ -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 ; case DialogType.INLINE_SYNTHESIS: return ; + case DialogType.OSS_SETTINGS: + return ; case DialogType.SHOW_AST: return ; case DialogType.SHOW_TYPE_GRAPH: diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx index c833233d..93468bcc 100644 --- a/rsconcept/frontend/src/components/icons.tsx +++ b/rsconcept/frontend/src/components/icons.tsx @@ -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'; diff --git a/rsconcept/frontend/src/components/input/checkbox.tsx b/rsconcept/frontend/src/components/input/checkbox.tsx index 85f223c8..ef699d68 100644 --- a/rsconcept/frontend/src/components/input/checkbox.tsx +++ b/rsconcept/frontend/src/components/input/checkbox.tsx @@ -14,10 +14,13 @@ export interface CheckboxProps extends Omit 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} > -
- {value ? : null} -
+ {customIcon ? ( + customIcon(value) + ) : ( +
+ {value ? : null} +
+ )} {label ? {label} : null} ); diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts index 1fffb566..69d7da8f 100644 --- a/rsconcept/frontend/src/features/oss/backend/api.ts +++ b/rsconcept/frontend/src/features/oss/backend/api.ts @@ -79,7 +79,7 @@ export const ossApi = { endpoint: `/api/oss/${itemID}/delete-block`, request: { data: data, - successMessage: infoMsg.operationDestroyed + successMessage: infoMsg.blockDestroyed } }), diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx index 25068835..aae89a6b 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-block.tsx @@ -51,7 +51,7 @@ export function DlgEditBlock() { submitText='Сохранить' canSubmit={isValid} onSubmit={event => void handleSubmit(onSubmit)(event)} - className='w-160 px-6 h-fit cc-column' + className='w-160 px-6 pb-2 h-fit cc-column' > 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 ( + + } + /> + } + /> + + checked ? ( + + ) : ( + + ) + } + /> + + checked ? ( + + ) : ( + + ) + } + /> + + ); +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx index f03c769f..e29e5be2 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx @@ -44,6 +44,7 @@ export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextM style={{ top: `calc(${cursorY}px - 2.5rem)`, left: cursorX }} > = window.innerWidth - MENU_WIDTH} stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx index 9185a55c..658ee67a 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx @@ -24,7 +24,7 @@ export function MenuBlock({ block, onHide }: MenuBlockProps) { const showEditBlock = useDialogsStore(state => state.showEditBlock); const { deleteBlock } = useDeleteBlock(); - function handleEditOperation() { + function handleEditBlock() { if (!block) { return; } @@ -47,7 +47,7 @@ export function MenuBlock({ block, onHide }: MenuBlockProps) { text='Редактировать' title='Редактировать блок' icon={} - onClick={handleEditOperation} + onClick={handleEditBlock} disabled={!isMutable || isProcessing} /> +
+
+
+
+
state.showCreateOperation); const showCreateBlock = useDialogsStore(state => state.showCreateBlock); const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); + const showEditBlock = useDialogsStore(state => state.showEditBlock); function onSelectionChange({ nodes }: { nodes: Node[] }) { const ids = nodes.map(node => Number(node.id)); @@ -215,13 +219,22 @@ export function OssFlow() { } function handleNodeDoubleClick(event: React.MouseEvent, node: OssNode) { - if (node.type === 'block') { - return; - } event.preventDefault(); event.stopPropagation(); - if (node.data.operation?.result) { - navigateOperationSchema(Number(node.id)); + + if (node.type === 'block') { + const block = schema.blockByID.get(-Number(node.id)); + if (block) { + showEditBlock({ + oss: schema, + target: block, + layout: getLayout() + }); + } + } else { + if (node.data.operation?.result) { + navigateOperationSchema(Number(node.id)); + } } } @@ -269,7 +282,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 +296,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 +336,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) { @@ -412,6 +431,7 @@ export function OssFlow() { onClick={() => setIsContextMenuOpen(false)} onNodeDoubleClick={handleNodeDoubleClick} onNodeContextMenu={handleContextMenu} + onContextMenu={event => event.preventDefault()} onNodeDragStart={handleDragStart} onNodeDrag={handleDrag} onNodeDragStop={handleDragStop} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx index 4171d45a..a5de6d46 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx @@ -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} /> - ) : ( - - ) - } - onClick={toggleShowGrid} + title='Исправить позиции узлов' + icon={} + onClick={handleFixLayout} + disabled={selected.length > 1 || selected[0] > 0} /> - ) : ( - - ) - } - onClick={toggleEdgeStraight} - /> - - ) : ( - - ) - } - onClick={toggleEdgeAnimate} - /> - } - onClick={toggleShowCoordinates} + title='Настройки отображения' + icon={} + onClick={handleShowOptions} />
diff --git a/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-ast/ast-flow.tsx b/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-ast/ast-flow.tsx index b54f9e0f..f1854526 100644 --- a/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-ast/ast-flow.tsx +++ b/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-ast/ast-flow.tsx @@ -69,6 +69,7 @@ export function ASTFlow({ data, onNodeEnter, onNodeLeave, onChangeDragging }: AS maxZoom={2} minZoom={0.5} nodesConnectable={false} + onContextMenu={event => event.preventDefault()} /> ); } diff --git a/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-type-graph/mgraph-flow.tsx b/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-type-graph/mgraph-flow.tsx index cf1b6a50..84d26b09 100644 --- a/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-type-graph/mgraph-flow.tsx +++ b/rsconcept/frontend/src/features/rsform/dialogs/dlg-show-type-graph/mgraph-flow.tsx @@ -69,6 +69,7 @@ export function MGraphFlow({ data }: MGraphFlowProps) { maxZoom={ZOOM_MAX} minZoom={ZOOM_MIN} nodesConnectable={false} + onContextMenu={event => event.preventDefault()} /> ); } diff --git a/rsconcept/frontend/src/hooks/use-throttle-callback.ts b/rsconcept/frontend/src/hooks/use-throttle-callback.ts new file mode 100644 index 00000000..c3bf450f --- /dev/null +++ b/rsconcept/frontend/src/hooks/use-throttle-callback.ts @@ -0,0 +1,24 @@ +'use client'; + +import { useCallback, useRef } from 'react'; + +/** Throttles a callback to only run once per delay. */ +export function useThrottleCallback void>( + callback: Callback, + delay: number +): Callback { + const lastCalled = useRef(0); + + const throttled = useCallback( + (...args: Parameters) => { + const now = Date.now(); + if (now - lastCalled.current >= delay) { + lastCalled.current = now; + callback(...args); + } + }, + [callback, delay] + ); + + return throttled as Callback; +} diff --git a/rsconcept/frontend/src/stores/dialogs.ts b/rsconcept/frontend/src/stores/dialogs.ts index 801330a1..87d1ed7e 100644 --- a/rsconcept/frontend/src/stores/dialogs.ts +++ b/rsconcept/frontend/src/stores/dialogs.ts @@ -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()(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 }), diff --git a/rsconcept/frontend/src/styling/components.css b/rsconcept/frontend/src/styling/components.css index 44d3c01c..4789c330 100644 --- a/rsconcept/frontend/src/styling/components.css +++ b/rsconcept/frontend/src/styling/components.css @@ -325,6 +325,7 @@ .selected & { color: var(--color-foreground); border-color: var(--color-graph-selected); + border-style: solid; } &:hover { diff --git a/rsconcept/frontend/src/styling/reactflow.css b/rsconcept/frontend/src/styling/reactflow.css index 7042a773..684857bb 100644 --- a/rsconcept/frontend/src/styling/reactflow.css +++ b/rsconcept/frontend/src/styling/reactflow.css @@ -157,5 +157,5 @@ margin: 0; background-color: transparent; - pointer-events: none; + pointer-events: none !important; } diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index cbf2a174..41d1cb5b 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -37,6 +37,7 @@ export const infoMsg = { versionDestroyed: 'Версия удалена', itemDestroyed: 'Схема удалена', operationDestroyed: 'Операция удалена', + blockDestroyed: 'Блок удален', operationExecuted: 'Операция выполнена', allOperationExecuted: 'Все операции выполнены', constituentsDestroyed: (count: number) => `Конституенты удалены: ${count}`