From 55fa09c6fb28d198d3175fac9f864c35b4f29172 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:16:06 +0300 Subject: [PATCH] R: Migrating to zustand for local state management pt3 --- .../frontend/src/app/ApplicationLayout.tsx | 4 ++ .../frontend/src/app/GlobalProviders.tsx | 4 +- rsconcept/frontend/src/app/GlobalTooltips.tsx | 32 +++++++++++ .../src/components/info/BadgeConstituenta.tsx | 6 +- .../src/context/ConceptOptionsContext.tsx | 56 ------------------- .../frontend/src/hooks/useLocalStorage.ts | 31 ---------- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 16 ++---- .../EditorOssGraph/ToolbarOssGraph.tsx | 23 ++++---- .../RSFormPage/EditorTermGraph/ViewHidden.tsx | 6 +- .../ViewConstituents/ConstituentsSearch.tsx | 42 +++++++------- rsconcept/frontend/src/stores/cstSearch.ts | 42 ++++++++++++++ rsconcept/frontend/src/stores/ossGraph.ts | 32 +++++++++++ rsconcept/frontend/src/stores/tooltips.ts | 13 +++++ rsconcept/frontend/src/utils/constants.ts | 15 ----- 14 files changed, 167 insertions(+), 155 deletions(-) create mode 100644 rsconcept/frontend/src/app/GlobalTooltips.tsx delete mode 100644 rsconcept/frontend/src/context/ConceptOptionsContext.tsx delete mode 100644 rsconcept/frontend/src/hooks/useLocalStorage.ts create mode 100644 rsconcept/frontend/src/stores/cstSearch.ts create mode 100644 rsconcept/frontend/src/stores/ossGraph.ts create mode 100644 rsconcept/frontend/src/stores/tooltips.ts diff --git a/rsconcept/frontend/src/app/ApplicationLayout.tsx b/rsconcept/frontend/src/app/ApplicationLayout.tsx index e9fe77ac..4b92bba5 100644 --- a/rsconcept/frontend/src/app/ApplicationLayout.tsx +++ b/rsconcept/frontend/src/app/ApplicationLayout.tsx @@ -9,6 +9,8 @@ import { NavigationState } from '@/context/NavigationContext'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { globals } from '@/utils/constants'; +import { GlobalTooltips } from './GlobalTooltips'; + function ApplicationLayout() { const mainHeight = useMainHeight(); const viewportHeight = useViewportHeight(); @@ -27,6 +29,8 @@ function ApplicationLayout() { pauseOnFocusLoss={false} /> + +
- + {children} @@ -43,7 +42,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) { - ); } diff --git a/rsconcept/frontend/src/app/GlobalTooltips.tsx b/rsconcept/frontend/src/app/GlobalTooltips.tsx new file mode 100644 index 00000000..fdbbc6a2 --- /dev/null +++ b/rsconcept/frontend/src/app/GlobalTooltips.tsx @@ -0,0 +1,32 @@ +'use client'; + +import InfoConstituenta from '@/components/info/InfoConstituenta'; +import Loader from '@/components/ui/Loader'; +import Tooltip from '@/components/ui/Tooltip'; +import { useTooltipsStore } from '@/stores/tooltips'; +import { globals } from '@/utils/constants'; + +export const GlobalTooltips = () => { + const hoverCst = useTooltipsStore(state => state.activeCst); + + return ( + <> + + + + {hoverCst ? event.stopPropagation()} /> : } + + + ); +}; diff --git a/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx b/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx index 7acaa4e6..aa457aa9 100644 --- a/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx +++ b/rsconcept/frontend/src/components/info/BadgeConstituenta.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; -import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { CstClass, IConstituenta } from '@/models/rsform'; +import { useTooltipsStore } from '@/stores/tooltips'; import { APP_COLORS, colorFgCstStatus } from '@/styling/color'; import { globals } from '@/utils/constants'; @@ -19,7 +19,7 @@ interface BadgeConstituentaProps extends CProps.Styling { * Displays a badge with a constituenta alias and information tooltip. */ function BadgeConstituenta({ value, prefixID, className, style }: BadgeConstituentaProps) { - const { setHoverCst } = useConceptOptions(); + const setActiveCst = useTooltipsStore(state => state.setActiveCst); return (
setHoverCst(value)} + onMouseEnter={() => setActiveCst(value)} > {value.alias}
diff --git a/rsconcept/frontend/src/context/ConceptOptionsContext.tsx b/rsconcept/frontend/src/context/ConceptOptionsContext.tsx deleted file mode 100644 index 8a6ebb4d..00000000 --- a/rsconcept/frontend/src/context/ConceptOptionsContext.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { createContext, useContext, useState } from 'react'; - -import InfoConstituenta from '@/components/info/InfoConstituenta'; -import Loader from '@/components/ui/Loader'; -import Tooltip from '@/components/ui/Tooltip'; -import { IConstituenta } from '@/models/rsform'; -import { globals } from '@/utils/constants'; -import { contextOutsideScope } from '@/utils/labels'; - -interface IOptionsContext { - setHoverCst: (newValue: IConstituenta | undefined) => void; -} - -const OptionsContext = createContext(null); -export const useConceptOptions = () => { - const context = useContext(OptionsContext); - if (!context) { - throw new Error(contextOutsideScope('useConceptTheme', 'ThemeState')); - } - return context; -}; - -export const OptionsState = ({ children }: React.PropsWithChildren) => { - const [hoverCst, setHoverCst] = useState(undefined); - - return ( - - <> - - - - {hoverCst ? event.stopPropagation()} /> : } - - - {children} - - - ); -}; diff --git a/rsconcept/frontend/src/hooks/useLocalStorage.ts b/rsconcept/frontend/src/hooks/useLocalStorage.ts deleted file mode 100644 index 8c78bdcd..00000000 --- a/rsconcept/frontend/src/hooks/useLocalStorage.ts +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; - -import { storage } from '@/utils/constants'; - -function useLocalStorage(key: string, defaultValue: ValueType | (() => ValueType)) { - const prefixedKey = `${storage.PREFIX}${key}`; - const [value, setValue] = useState(() => { - const loadedJson = localStorage.getItem(prefixedKey); - if (loadedJson != null) { - return JSON.parse(loadedJson) as ValueType; - } else if (typeof defaultValue === 'function') { - return (defaultValue as () => ValueType)(); - } else { - return defaultValue; - } - }); - - useEffect(() => { - if (value === undefined) { - localStorage.removeItem(prefixedKey); - } else { - localStorage.setItem(prefixedKey, JSON.stringify(value)); - } - }, [prefixedKey, value]); - - return [value, setValue] as [ValueType, typeof setValue]; -} - -export default useLocalStorage; diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index 2ffbb855..a03dd412 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -19,12 +19,12 @@ import { import { CProps } from '@/components/props'; import Overlay from '@/components/ui/Overlay'; import { useOSS } from '@/context/OssContext'; -import useLocalStorage from '@/hooks/useLocalStorage'; import { OssNode } from '@/models/miscellaneous'; import { OperationID } from '@/models/oss'; import { useMainHeight } from '@/stores/appLayout'; +import { useOSSGraphStore } from '@/stores/ossGraph'; import { APP_COLORS } from '@/styling/color'; -import { PARAMETER, storage } from '@/utils/constants'; +import { PARAMETER } from '@/utils/constants'; import { errors } from '@/utils/labels'; import { useOssEdit } from '../OssEditContext'; @@ -46,9 +46,9 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { 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 showGrid = useOSSGraphStore(state => state.showGrid); + const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); + const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -277,9 +277,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { flow.fitView({ duration: PARAMETER.zoomDuration })} onCreate={() => handleCreateOperation(controller.selected)} onDelete={handleDeleteSelected} @@ -288,9 +285,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { onResetPositions={() => setToggleReset(prev => !prev)} onSavePositions={handleSavePositions} onSaveImage={handleSaveImage} - toggleShowGrid={() => 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 ea622b9e..f9c112ac 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx @@ -21,6 +21,7 @@ import BadgeHelp from '@/components/info/BadgeHelp'; import MiniButton from '@/components/ui/MiniButton'; import { HelpTopic } from '@/models/miscellaneous'; import { OperationType } from '@/models/oss'; +import { useOSSGraphStore } from '@/stores/ossGraph'; import { PARAMETER } from '@/utils/constants'; import { prepareTooltip } from '@/utils/labels'; @@ -28,9 +29,6 @@ import { useOssEdit } from '../OssEditContext'; interface ToolbarOssGraphProps { isModified: boolean; - showGrid: boolean; - edgeAnimate: boolean; - edgeStraight: boolean; onCreate: () => void; onDelete: () => void; onEdit: () => void; @@ -39,16 +37,10 @@ interface ToolbarOssGraphProps { onSaveImage: () => void; onSavePositions: () => void; onResetPositions: () => void; - toggleShowGrid: () => void; - toggleEdgeAnimate: () => void; - toggleEdgeStraight: () => void; } function ToolbarOssGraph({ isModified, - showGrid, - edgeAnimate, - edgeStraight, onCreate, onDelete, onEdit, @@ -56,13 +48,18 @@ function ToolbarOssGraph({ onFitView, onSaveImage, onSavePositions, - onResetPositions, - toggleShowGrid, - toggleEdgeAnimate, - toggleEdgeStraight + onResetPositions }: ToolbarOssGraphProps) { const controller = useOssEdit(); const selectedOperation = controller.schema?.operationByID.get(controller.selected[0]); + + const showGrid = useOSSGraphStore(state => state.showGrid); + const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); + const edgeStraight = useOSSGraphStore(state => state.edgeStraight); + const toggleShowGrid = useOSSGraphStore(state => state.toggleShowGrid); + const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate); + const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight); + const readyForSynthesis = (() => { if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) { return false; diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx index 9c596cfa..df4424ac 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ViewHidden.tsx @@ -6,12 +6,12 @@ import { IconDropArrow, IconDropArrowUp } from '@/components/Icons'; import { CProps } from '@/components/props'; import MiniButton from '@/components/ui/MiniButton'; import Overlay from '@/components/ui/Overlay'; -import { useConceptOptions } from '@/context/ConceptOptionsContext'; import useWindowSize from '@/hooks/useWindowSize'; import { GraphColoring } from '@/models/miscellaneous'; import { ConstituentaID, IRSForm } from '@/models/rsform'; import { useFitHeight } from '@/stores/appLayout'; import { useTermGraphStore } from '@/stores/termGraph'; +import { useTooltipsStore } from '@/stores/tooltips'; import { APP_COLORS, colorBgGraphNode } from '@/styling/color'; import { globals, PARAMETER, prefixes } from '@/utils/constants'; @@ -32,7 +32,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori const isFolded = useTermGraphStore(state => state.foldHidden); const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden); - const { setHoverCst } = useConceptOptions(); + const setActiveCst = useTooltipsStore(state => state.setActiveCst); const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px'); function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) { @@ -110,7 +110,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori onClick={event => handleClick(cstID, event)} onDoubleClick={() => onEdit(cstID)} data-tooltip-id={globals.constituenta_tooltip} - onMouseEnter={() => setHoverCst(cst)} + onMouseEnter={() => setActiveCst(cst)} > {cst.alias} diff --git a/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsSearch.tsx b/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsSearch.tsx index 0c138ba6..49901dbf 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsSearch.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/ViewConstituents/ConstituentsSearch.tsx @@ -1,18 +1,16 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { IconChild } from '@/components/Icons'; import SelectGraphFilter from '@/components/select/SelectGraphFilter'; import SelectMatchMode from '@/components/select/SelectMatchMode'; import MiniButton from '@/components/ui/MiniButton'; import SearchBar from '@/components/ui/SearchBar'; -import useLocalStorage from '@/hooks/useLocalStorage'; -import { CstMatchMode, DependencyMode } from '@/models/miscellaneous'; import { applyGraphFilter } from '@/models/miscellaneousAPI'; import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { matchConstituenta } from '@/models/rsformAPI'; -import { storage } from '@/utils/constants'; +import { useCstSearchStore } from '@/stores/cstSearch'; interface ConstituentsSearchProps { schema?: IRSForm; @@ -23,10 +21,14 @@ interface ConstituentsSearchProps { } function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFiltered }: ConstituentsSearchProps) { - const [filterMatch, setFilterMatch] = useLocalStorage(storage.cstFilterMatch, CstMatchMode.ALL); - const [filterSource, setFilterSource] = useLocalStorage(storage.cstFilterGraph, DependencyMode.ALL); - const [filterText, setFilterText] = useState(''); - const [showInherited, setShowInherited] = useLocalStorage(storage.cstFilterShowInherited, true); + const query = useCstSearchStore(state => state.query); + const filterMatch = useCstSearchStore(state => state.match); + const filterSource = useCstSearchStore(state => state.source); + const includeInherited = useCstSearchStore(state => state.includeInherited); + const setQuery = useCstSearchStore(state => state.setQuery); + const setMatch = useCstSearchStore(state => state.setMatch); + const setSource = useCstSearchStore(state => state.setSource); + const toggleInherited = useCstSearchStore(state => state.toggleInherited); useEffect(() => { if (!schema || schema.items.length === 0) { @@ -39,15 +41,15 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt } else { result = applyGraphFilter(schema, activeID, filterSource); } - if (filterText) { - result = result.filter(cst => matchConstituenta(cst, filterText, filterMatch)); + if (query) { + result = result.filter(cst => matchConstituenta(cst, query, filterMatch)); } - if (!showInherited) { + if (!includeInherited) { result = result.filter(cst => !cst.is_inherited); } setFiltered(result); }, [ - filterText, + query, setFiltered, filterSource, activeExpression, @@ -55,7 +57,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt schema, filterMatch, activeID, - showInherited + includeInherited ]); return ( @@ -64,18 +66,18 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt id='constituents_search' noBorder className='min-w-[6rem] w-[6rem] mr-2 flex-grow' - query={filterText} - onChangeQuery={setFilterText} + query={query} + onChangeQuery={setQuery} /> - setFilterMatch(newValue)} dense={dense} /> - setFilterSource(newValue)} dense={dense} /> + setMatch(newValue)} dense={dense} /> + setSource(newValue)} dense={dense} /> {schema && schema?.stats.count_inherited > 0 ? ( ${showInherited ? 'отображать' : 'скрывать'}`} - icon={} + titleHtml={`Наследованные: ${includeInherited ? 'отображать' : 'скрывать'}`} + icon={} className='h-fit self-center' - onClick={() => setShowInherited(prev => !prev)} + onClick={toggleInherited} /> ) : null}
diff --git a/rsconcept/frontend/src/stores/cstSearch.ts b/rsconcept/frontend/src/stores/cstSearch.ts new file mode 100644 index 00000000..68b519a3 --- /dev/null +++ b/rsconcept/frontend/src/stores/cstSearch.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +import { CstMatchMode, DependencyMode } from '@/models/miscellaneous'; + +interface CstSearchStore { + query: string; + setQuery: (value: string) => void; + + match: CstMatchMode; + setMatch: (value: CstMatchMode) => void; + + source: DependencyMode; + setSource: (value: DependencyMode) => void; + + includeInherited: boolean; + toggleInherited: () => void; +} + +export const useCstSearchStore = create()( + persist( + set => ({ + query: '', + setQuery: value => set({ query: value }), + match: CstMatchMode.ALL, + setMatch: value => set({ match: value }), + source: DependencyMode.ALL, + setSource: value => set({ source: value }), + includeInherited: true, + toggleInherited: () => set(state => ({ includeInherited: !state.includeInherited })) + }), + { + version: 1, + partialize: state => ({ + match: state.match, + source: state.source, + includeInherited: state.includeInherited + }), + name: 'portal.constituenta.search' + } + ) +); diff --git a/rsconcept/frontend/src/stores/ossGraph.ts b/rsconcept/frontend/src/stores/ossGraph.ts new file mode 100644 index 00000000..dac9ff8a --- /dev/null +++ b/rsconcept/frontend/src/stores/ossGraph.ts @@ -0,0 +1,32 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface OSSGraphStore { + showGrid: boolean; + toggleShowGrid: () => void; + + edgeAnimate: boolean; + toggleEdgeAnimate: () => void; + + edgeStraight: boolean; + toggleEdgeStraight: () => void; +} + +export const useOSSGraphStore = create()( + persist( + set => ({ + showGrid: false, + toggleShowGrid: () => set(state => ({ showGrid: !state.showGrid })), + + edgeAnimate: false, + toggleEdgeAnimate: () => set(state => ({ edgeAnimate: !state.edgeAnimate })), + + edgeStraight: false, + toggleEdgeStraight: () => set(state => ({ edgeStraight: !state.edgeStraight })) + }), + { + version: 1, + name: 'portal.ossGraph' + } + ) +); diff --git a/rsconcept/frontend/src/stores/tooltips.ts b/rsconcept/frontend/src/stores/tooltips.ts new file mode 100644 index 00000000..2ad6449f --- /dev/null +++ b/rsconcept/frontend/src/stores/tooltips.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +import { IConstituenta } from '@/models/rsform'; + +interface TooltipsStore { + activeCst: IConstituenta | undefined; + setActiveCst: (value: IConstituenta | undefined) => void; +} + +export const useTooltipsStore = create()(set => ({ + activeCst: undefined, + setActiveCst: value => set({ activeCst: value }) +})); diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 82c6070b..ba7fd4b8 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -102,21 +102,6 @@ export const external_urls = { restAPI: 'https://api.portal.acconcept.ru' }; -/** - * Local storage ID. - */ -export const storage = { - PREFIX: 'portal.', - - ossShowGrid: 'oss.show_grid', - ossEdgeStraight: 'oss.edge_straight', - ossEdgeAnimate: 'oss.edge_animate', - - cstFilterMatch: 'cst.filter.match', - cstFilterGraph: 'cst.filter.graph', - cstFilterShowInherited: 'cst.filter.show_inherited' -}; - /** * Global element ID. */