From 4f9b48cce5d94eabd0743359e1ec4544ba794991 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Fri, 26 Jul 2024 21:09:16 +0300 Subject: [PATCH] F: Improve OSS UI controls --- .../src/app/Navigation/UserDropdown.tsx | 19 ++- rsconcept/frontend/src/app/urls.ts | 1 + rsconcept/frontend/src/components/Icons.tsx | 6 +- .../components/select/PickMultiOperation.tsx | 103 +++++++++++++ .../components/select/SelectConstituenta.tsx | 2 + .../src/components/select/SelectOperation.tsx | 2 + .../src/components/select/SelectUser.tsx | 4 +- .../src/components/select/SelectVersion.tsx | 3 + .../TabSynthesisOperation.tsx | 39 +---- .../frontend/src/models/miscellaneous.ts | 1 + rsconcept/frontend/src/pages/IconsPage.tsx | 17 +- .../OssPage/EditorOssGraph/EditorOssGraph.tsx | 7 +- .../OssPage/EditorOssGraph/InputNode.tsx | 2 +- .../OssPage/EditorOssGraph/OperationNode.tsx | 4 +- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 35 ++--- .../EditorOssGraph/ToolbarOssGraph.tsx | 145 +++++++++++------- rsconcept/frontend/src/utils/constants.ts | 2 + 17 files changed, 269 insertions(+), 123 deletions(-) create mode 100644 rsconcept/frontend/src/components/select/PickMultiOperation.tsx diff --git a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx index f1d6c8e2..8311fbb2 100644 --- a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx @@ -5,6 +5,7 @@ import { IconDatabase, IconHelp, IconHelpOff, + IconImage, IconLightTheme, IconLogout, IconUser @@ -43,6 +44,11 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { logout(() => router.push(urls.admin, true)); } + function gotoIcons(event: CProps.EventMouse) { + hideDropdown(); + router.push(urls.icons, event.ctrlKey || event.metaKey); + } + function handleToggleDarkMode() { hideDropdown(); toggleDarkMode(); @@ -77,7 +83,18 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { /> ) : null} {user?.is_staff ? ( - } onClick={gotoAdmin} /> + } + onClick={gotoAdmin} + /> + ) : null} + {user?.is_staff ? ( + } + onClick={gotoIcons} + /> ) : null} `/login?username=${userName}`, profile: `/${routes.profile}`, + icons: `/icons`, signup: `/${routes.signup}`, library: `/${routes.library}`, library_filter: (strategy: string) => `/library?filter=${strategy}`, diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx index 9b81c1ef..764844f6 100644 --- a/rsconcept/frontend/src/components/Icons.tsx +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -38,7 +38,6 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu'; export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu'; export { LuLightbulb as IconHelp } from 'react-icons/lu'; export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu'; -export { TbGridDots as IconGrid } from 'react-icons/tb'; export { RiPushpinFill as IconPin } from 'react-icons/ri'; export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; export { BiCaretDown as IconSortDesc } from 'react-icons/bi'; @@ -118,6 +117,11 @@ export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; export { LuSparkles as IconClustering } from 'react-icons/lu'; export { LuSparkle as IconClusteringOff } from 'react-icons/lu'; +export { TbGridDots as IconGrid } from 'react-icons/tb'; +export { FaSlash as IconLineStraight } from 'react-icons/fa6'; +export { PiWaveSineLight as IconLineWave } from 'react-icons/pi'; +export { LuCircleDashed as IconAnimation } from 'react-icons/lu'; +export { LuCircle as IconAnimationOff } from 'react-icons/lu'; // ===== Custom elements ====== interface IconSVGProps { diff --git a/rsconcept/frontend/src/components/select/PickMultiOperation.tsx b/rsconcept/frontend/src/components/select/PickMultiOperation.tsx new file mode 100644 index 00000000..13d89655 --- /dev/null +++ b/rsconcept/frontend/src/components/select/PickMultiOperation.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useCallback, useMemo, useState } from 'react'; + +import { IconRemove } from '@/components/Icons'; +import SelectOperation from '@/components/select/SelectOperation'; +import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; +import MiniButton from '@/components/ui/MiniButton'; +import NoData from '@/components/ui/NoData'; +import { IOperation, OperationID } from '@/models/oss'; + +interface PickMultiOperationProps { + rows?: number; + + items: IOperation[]; + selected: OperationID[]; + setSelected: React.Dispatch>; +} + +const columnHelper = createColumnHelper(); + +function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOperationProps) { + const selectedItems = useMemo(() => items.filter(item => selected.includes(item.id)), [items, selected]); + const nonSelectedItems = useMemo(() => items.filter(item => !selected.includes(item.id)), [items, selected]); + const [lastSelected, setLastSelected] = useState(undefined); + + const handleDelete = useCallback( + (operation: OperationID) => setSelected(prev => prev.filter(item => item !== operation)), + [setSelected] + ); + + const handleSelect = useCallback( + (operation?: IOperation) => { + if (operation) { + setLastSelected(operation); + setSelected(prev => [...prev, operation.id]); + setTimeout(() => setLastSelected(undefined), 1000); + } + }, + [setSelected] + ); + + const columns = useMemo( + () => [ + columnHelper.accessor('alias', { + id: 'alias', + header: 'Шифр', + size: 150, + minSize: 80, + maxSize: 150 + }), + columnHelper.accessor('title', { + id: 'title', + header: 'Название', + size: 1200, + minSize: 200, + maxSize: 1200, + cell: props =>
{props.getValue()}
+ }), + columnHelper.display({ + id: 'actions', + cell: props => ( + } + onClick={() => handleDelete(props.row.original.id)} + /> + ) + }) + ], + [handleDelete] + ); + + return ( +
+ + +

Список пуст

+ + } + /> +
+ ); +} + +export default PickMultiOperation; diff --git a/rsconcept/frontend/src/components/select/SelectConstituenta.tsx b/rsconcept/frontend/src/components/select/SelectConstituenta.tsx index a4bdc8cb..e302e011 100644 --- a/rsconcept/frontend/src/components/select/SelectConstituenta.tsx +++ b/rsconcept/frontend/src/components/select/SelectConstituenta.tsx @@ -15,7 +15,9 @@ interface SelectConstituentaProps extends CProps.Styling { items?: IConstituenta[]; value?: IConstituenta; onSelectValue: (newValue?: IConstituenta) => void; + placeholder?: string; + noBorder?: boolean; } function SelectConstituenta({ diff --git a/rsconcept/frontend/src/components/select/SelectOperation.tsx b/rsconcept/frontend/src/components/select/SelectOperation.tsx index e51f00ef..50416d5d 100644 --- a/rsconcept/frontend/src/components/select/SelectOperation.tsx +++ b/rsconcept/frontend/src/components/select/SelectOperation.tsx @@ -13,7 +13,9 @@ interface SelectOperationProps extends CProps.Styling { items?: IOperation[]; value?: IOperation; onSelectValue: (newValue?: IOperation) => void; + placeholder?: string; + noBorder?: boolean; } function SelectOperation({ diff --git a/rsconcept/frontend/src/components/select/SelectUser.tsx b/rsconcept/frontend/src/components/select/SelectUser.tsx index 3cd0c7d2..5b7c4fe7 100644 --- a/rsconcept/frontend/src/components/select/SelectUser.tsx +++ b/rsconcept/frontend/src/components/select/SelectUser.tsx @@ -13,8 +13,10 @@ import SelectSingle from '../ui/SelectSingle'; interface SelectUserProps extends CProps.Styling { items?: IUserInfo[]; value?: UserID; - placeholder?: string; onSelectValue: (newValue: UserID) => void; + + placeholder?: string; + noBorder?: boolean; } function SelectUser({ diff --git a/rsconcept/frontend/src/components/select/SelectVersion.tsx b/rsconcept/frontend/src/components/select/SelectVersion.tsx index e4cb349e..47198335 100644 --- a/rsconcept/frontend/src/components/select/SelectVersion.tsx +++ b/rsconcept/frontend/src/components/select/SelectVersion.tsx @@ -14,6 +14,9 @@ interface SelectVersionProps extends CProps.Styling { items?: IVersionInfo[]; value?: VersionID; onSelectValue: (newValue?: VersionID) => void; + + placeholder?: string; + noBorder?: boolean; } function SelectVersion({ id, className, items, value, onSelectValue, ...restProps }: SelectVersionProps) { diff --git a/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabSynthesisOperation.tsx b/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabSynthesisOperation.tsx index 03117d0a..24c208ca 100644 --- a/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabSynthesisOperation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabSynthesisOperation.tsx @@ -1,16 +1,13 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import SelectOperation from '@/components/select/SelectOperation'; import FlexColumn from '@/components/ui/FlexColumn'; import Label from '@/components/ui/Label'; import TextArea from '@/components/ui/TextArea'; import TextInput from '@/components/ui/TextInput'; import AnimateFade from '@/components/wrap/AnimateFade'; -import { IOperation, IOperationSchema, OperationID } from '@/models/oss'; +import { IOperationSchema, OperationID } from '@/models/oss'; import { limits, patterns } from '@/utils/constants'; +import PickMultiOperation from '../../components/select/PickMultiOperation'; + interface TabSynthesisOperationProps { oss: IOperationSchema; alias: string; @@ -34,22 +31,6 @@ function TabSynthesisOperation({ inputs, setInputs }: TabSynthesisOperationProps) { - const [left, setLeft] = useState(undefined); - const [right, setRight] = useState(undefined); - - console.log(inputs); - - useEffect(() => { - const inputs: OperationID[] = []; - if (left) { - inputs.push(left.id); - } - if (right) { - inputs.push(right.id); - } - setInputs(inputs); - }, [setInputs, left, right]); - return ( -
- - - - -
+ +
); } diff --git a/rsconcept/frontend/src/models/miscellaneous.ts b/rsconcept/frontend/src/models/miscellaneous.ts index 6632fa98..584a38a2 100644 --- a/rsconcept/frontend/src/models/miscellaneous.ts +++ b/rsconcept/frontend/src/models/miscellaneous.ts @@ -40,6 +40,7 @@ export interface OssNodeInternal { label: string; operation: IOperation; }; + dragging: boolean; xPos: number; yPos: number; } diff --git a/rsconcept/frontend/src/pages/IconsPage.tsx b/rsconcept/frontend/src/pages/IconsPage.tsx index 9c2ebed8..41988ede 100644 --- a/rsconcept/frontend/src/pages/IconsPage.tsx +++ b/rsconcept/frontend/src/pages/IconsPage.tsx @@ -4,18 +4,17 @@ import * as icons from '@/components/Icons'; export function IconsPage() { + const iconsList = Object.keys(icons).filter(key => key.startsWith('Icon')); return (
-

Список иконок

+

Всего иконок: {iconsList.length}

- {Object.keys(icons) - .filter(key => key.startsWith('Icon')) - .map((key, index) => ( -
-

{icons[key]({ size: '2rem' })}

-

{key}

-
- ))} + {iconsList.map((key, index) => ( +
+

{icons[key]({ size: '2rem' })}

+

{key}

+
+ ))}
); diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx index c8dcad0c..16f595be 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx @@ -2,9 +2,6 @@ import { ReactFlowProvider } from 'reactflow'; -import useLocalStorage from '@/hooks/useLocalStorage'; -import { storage } from '@/utils/constants'; - import OssFlow from './OssFlow'; interface EditorOssGraphProps { @@ -13,11 +10,9 @@ interface EditorOssGraphProps { } function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) { - const [showGrid, setShowGrid] = useLocalStorage(storage.ossShowGrid, false); - return ( - + ); } diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx index 148ead96..6a6bfad7 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx @@ -34,7 +34,7 @@ function InputNode(node: OssNodeInternal) {
{node.data.label} - {controller.showTooltip ? ( + {controller.showTooltip && !node.dragging ? ( ) : null}
diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx index c2dec0d8..dbe1a076 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx @@ -36,7 +36,9 @@ function OperationNode(node: OssNodeInternal) {
{node.data.label} - + {controller.showTooltip && !node.dragging ? ( + + ) : null}
diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index 64072b3e..d7421dcf 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -10,7 +10,6 @@ import { Node, NodeChange, NodeTypes, - ProOptions, ReactFlow, useEdgesState, useNodesState, @@ -23,9 +22,10 @@ import Overlay from '@/components/ui/Overlay'; import AnimateFade from '@/components/wrap/AnimateFade'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useOSS } from '@/context/OssContext'; +import useLocalStorage from '@/hooks/useLocalStorage'; import { OssNode } from '@/models/miscellaneous'; import { OperationID } from '@/models/oss'; -import { PARAMETER } from '@/utils/constants'; +import { PARAMETER, storage } from '@/utils/constants'; import { errors } from '@/utils/labels'; import { useOssEdit } from '../OssEditContext'; @@ -37,16 +37,18 @@ import ToolbarOssGraph from './ToolbarOssGraph'; interface OssFlowProps { isModified: boolean; setIsModified: React.Dispatch>; - showGrid: boolean; - setShowGrid: React.Dispatch>; } -function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) { +function OssFlow({ isModified, setIsModified }: OssFlowProps) { const { calculateHeight, colors } = useConceptOptions(); const model = useOSS(); const controller = useOssEdit(); const flow = useReactFlow(); + const [showGrid, setShowGrid] = useLocalStorage(storage.ossShowGrid, false); + const [edgeAnimate, setEdgeAnimate] = useLocalStorage(storage.ossEdgeAnimate, false); + const [edgeStraight, setEdgeStraight] = useLocalStorage(storage.ossEdgeStraight, false); + const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [toggleReset, setToggleReset] = useState(false); @@ -81,6 +83,8 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr id: String(index), source: String(argument.argument), target: String(argument.operation), + type: edgeStraight ? 'straight' : 'bezier', + animated: edgeAnimate, targetHandle: model.schema!.operationByID.get(argument.argument)!.position_x > model.schema!.operationByID.get(argument.operation)!.position_x @@ -92,7 +96,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr setTimeout(() => { setIsModified(false); }, PARAMETER.graphRefreshDelay); - }, [model.schema, setNodes, setEdges, setIsModified, toggleReset]); + }, [model.schema, setNodes, setEdges, setIsModified, toggleReset, edgeStraight, edgeAnimate]); const getPositions = useCallback( () => @@ -224,7 +228,6 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr } } - const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []); const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []); const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); @@ -244,7 +247,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr onNodesChange={handleNodesChange} onEdgesChange={onEdgesChange} fitView - proOptions={proOptions} + proOptions={{ hideAttribution: true }} nodeTypes={OssNodeTypes} maxZoom={2} minZoom={0.75} @@ -257,17 +260,7 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr {showGrid ? : null} ), - [ - nodes, - edges, - proOptions, - handleNodesChange, - handleContextMenu, - handleClickCanvas, - onEdgesChange, - OssNodeTypes, - showGrid - ] + [nodes, edges, handleNodesChange, handleContextMenu, handleClickCanvas, onEdgesChange, OssNodeTypes, showGrid] ); return ( @@ -276,6 +269,8 @@ function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowPr setShowGrid(prev => !prev)} + toggleEdgeAnimate={() => setEdgeAnimate(prev => !prev)} + toggleEdgeStraight={() => setEdgeStraight(prev => !prev)} /> {menuProps ? ( diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx index fa69f52f..c025f0b7 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx @@ -1,6 +1,18 @@ import clsx from 'clsx'; -import { IconDestroy, IconFitImage, IconGrid, IconImage, IconNewItem, IconReset, IconSave } from '@/components/Icons'; +import { + IconAnimation, + IconAnimationOff, + IconDestroy, + IconFitImage, + IconGrid, + IconImage, + IconLineStraight, + IconLineWave, + IconNewItem, + IconReset, + IconSave +} from '@/components/Icons'; import BadgeHelp from '@/components/info/BadgeHelp'; import MiniButton from '@/components/ui/MiniButton'; import { HelpTopic } from '@/models/miscellaneous'; @@ -12,6 +24,8 @@ import { useOssEdit } from '../OssEditContext'; interface ToolbarOssGraphProps { isModified: boolean; showGrid: boolean; + edgeAnimate: boolean; + edgeStraight: boolean; onCreate: () => void; onDelete: () => void; onFitView: () => void; @@ -19,81 +33,108 @@ interface ToolbarOssGraphProps { onSavePositions: () => void; onResetPositions: () => void; toggleShowGrid: () => void; + toggleEdgeAnimate: () => void; + toggleEdgeStraight: () => void; } function ToolbarOssGraph({ isModified, showGrid, + edgeAnimate, + edgeStraight, onCreate, onDelete, onFitView, onSaveImage, onSavePositions, onResetPositions, - toggleShowGrid + toggleShowGrid, + toggleEdgeAnimate, + toggleEdgeStraight }: ToolbarOssGraphProps) { const controller = useOssEdit(); return ( -
- {controller.isMutable ? ( +
+
} - disabled={controller.isProcessing || !isModified} - onClick={onSavePositions} + icon={} + title='Сбросить вид' + onClick={onFitView} /> - ) : null} - {controller.isMutable ? ( } - disabled={!isModified} - onClick={onResetPositions} + title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'} + icon={ + showGrid ? ( + + ) : ( + + ) + } + onClick={toggleShowGrid} /> - ) : null} - } - title='Сбросить вид' - onClick={onFitView} - /> - - ) : ( - - ) - } - onClick={toggleShowGrid} - /> - {controller.isMutable ? ( } - disabled={controller.isProcessing} - onClick={onCreate} + title={edgeStraight ? 'Связи: прямые' : 'Связи: безье'} + icon={ + edgeStraight ? ( + + ) : ( + + ) + } + onClick={toggleEdgeStraight} /> - ) : null} - {controller.isMutable ? ( } - disabled={controller.selected.length !== 1 || controller.isProcessing} - onClick={onDelete} + title={edgeAnimate ? 'Анимация: вкл' : 'Анимация: выкл'} + icon={ + edgeAnimate ? ( + + ) : ( + + ) + } + onClick={toggleEdgeAnimate} /> + } + title='Сохранить изображение' + onClick={onSaveImage} + /> + +
+ {controller.isMutable ? ( +
+ {' '} + } + disabled={controller.isProcessing || !isModified} + onClick={onSavePositions} + /> + } + disabled={!isModified} + onClick={onResetPositions} + /> + } + disabled={controller.isProcessing} + onClick={onCreate} + /> + } + disabled={controller.selected.length !== 1 || controller.isProcessing} + onClick={onDelete} + /> +
) : null} - } - title='Сохранить изображение' - onClick={onSaveImage} - /> -
); } diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 5f1f4955..fe71fa73 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -114,6 +114,8 @@ export const storage = { rsgraphFoldHidden: 'rsgraph.fold_hidden', ossShowGrid: 'oss.show_grid', + ossEdgeStraight: 'oss.edge_straight', + ossEdgeAnimate: 'oss.edge_animate', cstFilterMatch: 'cst.filter.match', cstFilterGraph: 'cst.filter.graph'