R: Migrating to zustand for local state management pt1
Some checks are pending
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2025-01-14 21:58:16 +03:00
parent 5679c86dac
commit 4c9b0b28b8
34 changed files with 357 additions and 168 deletions

View File

@ -44,6 +44,7 @@ This readme file is used mostly to document project dependencies and conventions
- use-debounce
- qrcode.react
- html-to-image
- zustand
- @tanstack/react-table
- @uiw/react-codemirror
- @uiw/codemirror-themes

View File

@ -30,7 +30,8 @@
"react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4"
"use-debounce": "^10.0.4",
"zustand": "^5.0.3"
},
"devDependencies": {
"@lezer/generator": "^1.7.2",
@ -2445,6 +2446,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/background/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/controls": {
"version": "11.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
@ -2460,6 +2489,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/controls/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/core": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
@ -2481,6 +2538,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/core/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/minimap": {
"version": "11.7.14",
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
@ -2500,6 +2585,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/minimap/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-resizer": {
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
@ -2517,6 +2630,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-toolbar": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
@ -2532,6 +2673,34 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.30.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
@ -10711,20 +10880,18 @@
}
},
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
"integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=16.8"
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
@ -10735,6 +10902,9 @@
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}

View File

@ -34,7 +34,8 @@
"react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4"
"use-debounce": "^10.0.4",
"zustand": "^5.0.3"
},
"devDependencies": {
"@lezer/generator": "^1.7.2",

View File

@ -5,12 +5,17 @@ import ConceptToaster from '@/app/ConceptToaster';
import Footer from '@/app/Footer';
import Navigation from '@/app/Navigation';
import Loader from '@/components/ui/Loader';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { NavigationState } from '@/context/NavigationContext';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants';
function ApplicationLayout() {
const { viewportHeight, mainHeight, showScroll, noNavigationAnimation } = useConceptOptions();
const mainHeight = useMainHeight();
const viewportHeight = useViewportHeight();
const showScroll = useAppLayoutStore(state => !state.noScroll);
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter);
return (
<NavigationState>
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
@ -36,7 +41,7 @@ function ApplicationLayout() {
<Outlet />
</Suspense>
</main>
<Footer />
{!noNavigation && !noFooter ? <Footer /> : null}
</div>
</div>
</NavigationState>

View File

@ -1,15 +1,10 @@
import clsx from 'clsx';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { external_urls } from '@/utils/constants';
import TextURL from '../components/ui/TextURL';
function Footer() {
const { noNavigation, noFooter } = useConceptOptions();
if (noNavigation || noFooter) {
return null;
}
return (
<footer
className={clsx(

View File

@ -2,9 +2,9 @@ import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { CProps } from '@/components/props';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useWindowSize from '@/hooks/useWindowSize';
import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls';
@ -16,7 +16,7 @@ import UserMenu from './UserMenu';
function Navigation() {
const router = useConceptNavigation();
const size = useWindowSize();
const { noNavigationAnimation } = useConceptOptions();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey);
const navigateLibrary = (event: CProps.EventMouse) => router.push(urls.library, event.ctrlKey || event.metaKey);

View File

@ -2,10 +2,14 @@ import clsx from 'clsx';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useAppLayoutStore } from '@/stores/appLayout';
import { globals, PARAMETER } from '@/utils/constants';
function ToggleNavigation() {
const { noNavigationAnimation, noNavigation, toggleNoNavigation, toggleDarkMode, darkMode } = useConceptOptions();
const { toggleDarkMode, darkMode } = useConceptOptions();
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
const iconSize = !noNavigationAnimation ? '0.75rem' : '1rem';
return (
<div

View File

@ -18,6 +18,7 @@ import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls';
@ -27,10 +28,15 @@ interface UserDropdownProps {
}
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions();
const { darkMode, toggleDarkMode } = useConceptOptions();
const router = useConceptNavigation();
const { user, logout } = useAuth();
const showHelp = usePreferencesStore(state => state.showHelp);
const toggleShowHelp = usePreferencesStore(state => state.toggleShowHelp);
const adminMode = usePreferencesStore(state => state.adminMode);
const toggleAdminMode = usePreferencesStore(state => state.toggleAdminMode);
function navigateProfile(event: CProps.EventMouse) {
hideDropdown();
router.push(urls.profile, event.ctrlKey || event.metaKey);

View File

@ -1,9 +1,9 @@
import { IconLogin, IconUser2 } from '@/components/Icons';
import Loader from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useDropdown from '@/hooks/useDropdown';
import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls';
import NavigationButton from './NavigationButton';
@ -12,7 +12,7 @@ import UserDropdown from './UserDropdown';
function UserMenu() {
const router = useConceptNavigation();
const { user, loading } = useAuth();
const { adminMode } = useConceptOptions();
const adminMode = usePreferencesStore(state => state.adminMode);
const menu = useDropdown();
const navigateLogin = () => router.push(urls.login);

View File

@ -2,8 +2,8 @@ import React, { Suspense } from 'react';
import TextURL from '@/components/ui/TextURL';
import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous';
import { usePreferencesStore } from '@/stores/preferences';
import { IconHelp } from '../Icons';
import { CProps } from '../props';
@ -29,7 +29,7 @@ interface BadgeHelpProps extends CProps.Styling {
* Display help icon with a manual page tooltip.
*/
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
const { showHelp } = useConceptOptions();
const showHelp = usePreferencesStore(state => state.showHelp);
if (!showHelp) {
return null;

View File

@ -1,7 +1,7 @@
'use client';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout';
/** Maximum width of the viewer. */
const MAXIMUM_WIDTH = 1600;
@ -25,10 +25,9 @@ interface PDFViewerProps {
*/
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
const windowSize = useWindowSize();
const { calculateHeight } = useConceptOptions();
const pageWidth = Math.max(minWidth, Math.min((windowSize?.width ?? 0) - (offsetXpx ?? 0) - 10, MAXIMUM_WIDTH));
const pageHeight = calculateHeight('1rem');
const pageHeight = useFitHeight('1rem');
return <embed src={`${file}#toolbar=0`} className='p-3' style={{ width: pageWidth, height: pageHeight }} />;
}

View File

@ -1,6 +1,6 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import InfoConstituenta from '@/components/info/InfoConstituenta';
@ -12,28 +12,9 @@ import { globals, PARAMETER, storage } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels';
interface IOptionsContext {
viewportHeight: string;
mainHeight: string;
darkMode: boolean;
toggleDarkMode: () => void;
adminMode: boolean;
toggleAdminMode: () => void;
noNavigationAnimation: boolean;
noNavigation: boolean;
toggleNoNavigation: () => void;
noFooter: boolean;
setNoFooter: React.Dispatch<React.SetStateAction<boolean>>;
showScroll: boolean;
setShowScroll: React.Dispatch<React.SetStateAction<boolean>>;
showHelp: boolean;
toggleShowHelp: () => void;
folderMode: boolean;
setFolderMode: React.Dispatch<React.SetStateAction<boolean>>;
@ -41,8 +22,6 @@ interface IOptionsContext {
setLocation: React.Dispatch<React.SetStateAction<string>>;
setHoverCst: (newValue: IConstituenta | undefined) => void;
calculateHeight: (offset: string, minimum?: string) => string;
}
const OptionsContext = createContext<IOptionsContext | null>(null);
@ -56,17 +35,10 @@ export const useConceptOptions = () => {
export const OptionsState = ({ children }: React.PropsWithChildren) => {
const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false);
const [adminMode, setAdminMode] = useLocalStorage(storage.optionsAdmin, false);
const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
const [noNavigation, setNoNavigation] = useState(false);
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
const [noNavigationAnimation, setNoNavigationAnimation] = useState(false);
const [noFooter, setNoFooter] = useState(false);
const [showScroll, setShowScroll] = useState(false);
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
function setDarkClass(isDark: boolean) {
@ -83,29 +55,6 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
setDarkClass(darkMode);
}, [darkMode]);
const toggleNoNavigation = useCallback(() => {
if (noNavigation) {
setNoNavigationAnimation(false);
setNoNavigation(false);
} else {
setNoNavigationAnimation(true);
setTimeout(() => setNoNavigation(true), PARAMETER.moveDuration);
}
}, [noNavigation]);
const calculateHeight = useCallback(
(offset: string, minimum: string = '0px') => {
if (noNavigation) {
return `max(calc(100dvh - (${offset})), ${minimum})`;
} else if (noFooter) {
return `max(calc(100dvh - 3rem - (${offset})), ${minimum})`;
} else {
return `max(calc(100dvh - 6.75rem - (${offset})), ${minimum})`;
}
},
[noNavigation, noFooter]
);
const toggleDarkMode = useCallback(() => {
if (!document.startViewTransition) {
setDarkMode(prev => !prev);
@ -129,43 +78,15 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
}
}, [setDarkMode]);
const mainHeight = useMemo(() => {
if (noNavigation) {
return '100dvh';
} else if (noFooter) {
return 'calc(100dvh - 3rem)';
} else {
return 'calc(100dvh - 6.75rem)';
}
}, [noNavigation, noFooter]);
const viewportHeight = useMemo(() => {
return !noNavigation ? 'calc(100dvh - 3rem)' : '100dvh';
}, [noNavigation]);
return (
<OptionsContext
value={{
darkMode,
adminMode,
noNavigationAnimation,
noNavigation,
noFooter,
folderMode,
setFolderMode,
location,
setLocation,
showScroll,
showHelp,
toggleDarkMode: toggleDarkMode,
toggleAdminMode: () => setAdminMode(prev => !prev),
toggleNoNavigation: toggleNoNavigation,
setNoFooter,
setShowScroll,
toggleShowHelp: () => setShowHelp(prev => !prev),
viewportHeight,
mainHeight,
calculateHeight,
setHoverCst
}}
>

View File

@ -21,10 +21,10 @@ import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI'
import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader';
import { usePreferencesStore } from '@/stores/preferences';
import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useConceptOptions } from './ConceptOptionsContext';
interface ILibraryContext {
items: ILibraryItem[];
@ -63,7 +63,7 @@ export const useLibrary = (): ILibraryContext => {
export const LibraryState = ({ children }: React.PropsWithChildren) => {
const { user, loading: userLoading } = useAuth();
const { adminMode } = useConceptOptions();
const adminMode = usePreferencesStore(state => state.adminMode);
const [items, setItems] = useState<ILibraryItem[]>([]);
const [templates, setTemplates] = useState<ILibraryItem[]>([]);

View File

@ -3,18 +3,18 @@
import { useEffect } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
import { resources } from '@/utils/constants';
function DatabaseSchemaPage() {
const { calculateHeight, setNoFooter } = useConceptOptions();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const panelHeight = calculateHeight('0px');
const panelHeight = useFitHeight('0px');
useEffect(() => {
setNoFooter(true);
return () => setNoFooter(false);
}, [setNoFooter]);
hideFooter(true);
return () => hideFooter(false);
}, [hideFooter]);
return (
<div className='cc-fade-in flex justify-center overflow-hidden' style={{ maxHeight: panelHeight }}>

View File

@ -16,6 +16,7 @@ import useLocalStorage from '@/hooks/useLocalStorage';
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
import { ILibraryFilter } from '@/models/miscellaneous';
import { UserID } from '@/models/user';
import { useAppLayoutStore } from '@/stores/appLayout';
import { storage } from '@/utils/constants';
import { information } from '@/utils/labels';
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
@ -29,6 +30,7 @@ function LibraryPage() {
const { user } = useAuth();
const [items, setItems] = useState<ILibraryItem[]>([]);
const options = useConceptOptions();
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const [query, setQuery] = useState('');
const [path, setPath] = useState('');
@ -133,7 +135,7 @@ function LibraryPage() {
/>
) : null}
<Overlay
position={options.noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
position={noNavigation ? 'top-[0.25rem] right-[3rem]' : 'top-[0.25rem] right-0'}
layer='z-tooltip'
className='cc-animate-position'
>

View File

@ -12,12 +12,12 @@ import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } fro
import FlexColumn from '@/components/ui/FlexColumn';
import MiniButton from '@/components/ui/MiniButton';
import TextURL from '@/components/ui/TextURL';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useUsers } from '@/context/UsersContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { ILibraryItem, LibraryItemType } from '@/models/library';
import { useFitHeight } from '@/stores/appLayout';
import { APP_COLORS } from '@/styling/color';
import { storage } from '@/utils/constants';
@ -34,7 +34,6 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
const router = useConceptNavigation();
const intl = useIntl();
const { getUserLabel } = useUsers();
const { calculateHeight } = useConceptOptions();
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
@ -140,7 +139,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
})
];
const tableHeight = calculateHeight('2.2rem');
const tableHeight = useFitHeight('2.2rem');
const conditionalRowStyles: IConditionalStyle<ILibraryItem>[] = [
{

View File

@ -8,11 +8,11 @@ import { CProps } from '@/components/props';
import SelectLocation from '@/components/select/SelectLocation';
import MiniButton from '@/components/ui/MiniButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import useWindowSize from '@/hooks/useWindowSize';
import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous';
import { useFitHeight } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information } from '@/utils/labels';
@ -39,7 +39,6 @@ function ViewSideLocation({
}: ViewSideLocationProps) {
const { user } = useAuth();
const { items } = useLibrary();
const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize();
const canRename = (() => {
@ -56,7 +55,7 @@ function ViewSideLocation({
return located.length !== 0;
})();
const maxHeight = calculateHeight('4.5rem');
const maxHeight = useFitHeight('4.5rem');
function handleClickFolder(event: CProps.EventMouse, target: FolderNode) {
event.preventDefault();

View File

@ -3,10 +3,10 @@
import { useCallback } from 'react';
import { urls } from '@/app/urls';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { HelpTopic } from '@/models/miscellaneous';
import { useMainHeight } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants';
import TopicsList from './TopicsList';
@ -17,7 +17,7 @@ function ManualsPage() {
const query = useQueryStrings();
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
const { mainHeight } = useConceptOptions();
const mainHeight = useMainHeight();
const onSelectTopic = useCallback(
(newTopic: HelpTopic) => {

View File

@ -6,9 +6,9 @@ import { useCallback } from 'react';
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
import Button from '@/components/ui/Button';
import SelectTree from '@/components/ui/SelectTree';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useDropdown from '@/hooks/useDropdown';
import { HelpTopic, topicParent } from '@/models/miscellaneous';
import { useAppLayoutStore, useFitHeight } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
@ -19,7 +19,8 @@ interface TopicsDropdownProps {
function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
const menu = useDropdown();
const { noNavigation, calculateHeight } = useConceptOptions();
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const treeHeight = useFitHeight('4rem + 2px');
const handleSelectTopic = useCallback(
(topic: HelpTopic) => {
@ -67,7 +68,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
'bg-prim-200'
)}
style={{
maxHeight: calculateHeight('4rem + 2px'),
maxHeight: treeHeight,
transitionProperty: 'clip-path',
transitionDuration: `${PARAMETER.moveDuration}ms`,
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'

View File

@ -1,8 +1,8 @@
import clsx from 'clsx';
import SelectTree from '@/components/ui/SelectTree';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic, topicParent } from '@/models/miscellaneous';
import { useFitHeight } from '@/stores/appLayout';
import { prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
@ -12,7 +12,7 @@ interface TopicsStaticProps {
}
function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
const { calculateHeight } = useConceptOptions();
const topicsHeight = useFitHeight('1rem + 2px');
return (
<SelectTree
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
@ -31,7 +31,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
'text-xs sm:text-sm bg-prim-200',
'select-none'
)}
style={{ maxHeight: calculateHeight('1rem + 2px') }}
style={{ maxHeight: topicsHeight }}
/>
);
}

View File

@ -1,15 +1,15 @@
'use client';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { HelpTopic } from '@/models/miscellaneous';
import TopicPage from '@/pages/ManualsPage/TopicPage';
import { useMainHeight } from '@/stores/appLayout';
interface ViewTopicProps {
topic: HelpTopic;
}
function ViewTopic({ topic }: ViewTopicProps) {
const { mainHeight } = useConceptOptions();
const mainHeight = useMainHeight();
return (
<div
key={topic}

View File

@ -18,11 +18,11 @@ import {
import { CProps } from '@/components/props';
import Overlay from '@/components/ui/Overlay';
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 { useMainHeight } from '@/stores/appLayout';
import { APP_COLORS } from '@/styling/color';
import { PARAMETER, storage } from '@/utils/constants';
import { errors } from '@/utils/labels';
@ -41,7 +41,7 @@ interface OssFlowProps {
}
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
const { mainHeight } = useConceptOptions();
const mainHeight = useMainHeight();
const model = useOSS();
const controller = useOssEdit();
const flow = useReactFlow();

View File

@ -6,7 +6,6 @@ import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';
@ -32,6 +31,7 @@ import {
OperationType
} from '@/models/oss';
import { UserID, UserLevel } from '@/models/user';
import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants';
import { errors, information } from '@/utils/labels';
@ -95,7 +95,7 @@ interface OssEditStateProps {
export const OssEditState = ({ selected, setSelected, children }: React.PropsWithChildren<OssEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const { adminMode } = useConceptOptions();
const adminMode = usePreferencesStore(state => state.adminMode);
const { accessLevel, setAccessLevel } = useAccessMode();
const model = useOSS();
const library = useLibrary();

View File

@ -13,12 +13,12 @@ import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { OperationID } from '@/models/oss';
import { useAppLayoutStore } from '@/stores/appLayout';
import { information, prompts } from '@/utils/labels';
import EditorRSForm from './EditorOssCard';
@ -37,7 +37,7 @@ function OssTabs() {
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
const { user } = useAuth();
const { setNoFooter } = useConceptOptions();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, loadingError: errorLoading } = useOSS();
const { destroyItem } = useLibrary();
@ -61,8 +61,8 @@ function OssTabs() {
}, [schema, schema?.title]);
useEffect(() => {
setNoFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, setNoFooter]);
hideFooter(activeTab === OssTabID.GRAPH);
}, [activeTab, hideFooter]);
function navigateTab(tab: OssTabID) {
if (!schema) {

View File

@ -3,10 +3,10 @@
import clsx from 'clsx';
import { useState } from 'react';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { useMainHeight } from '@/stores/appLayout';
import { globals, storage } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
@ -27,7 +27,7 @@ interface EditorConstituentaProps {
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
const controller = useRSEdit();
const windowSize = useWindowSize();
const { mainHeight } = useConceptOptions();
const mainHeight = useMainHeight();
const [showList, setShowList] = useLocalStorage(storage.rseditShowList, true);
const [toggleReset, setToggleReset] = useState(false);

View File

@ -9,10 +9,10 @@ import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import SearchBar from '@/components/ui/SearchBar';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { CstMatchMode } from '@/models/miscellaneous';
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
import { matchConstituenta } from '@/models/rsformAPI';
import { useFitHeight } from '@/stores/appLayout';
import { information } from '@/utils/labels';
import { convertToCSV } from '@/utils/utils';
@ -25,7 +25,6 @@ interface EditorRSListProps {
}
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
const { calculateHeight } = useConceptOptions();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit();
@ -136,7 +135,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
return false;
}
const tableHeight = calculateHeight('4.05rem + 5px');
const tableHeight = useFitHeight('4.05rem + 5px');
return (
<>

View File

@ -24,12 +24,12 @@ import SelectedCounter from '@/components/info/SelectedCounter';
import { CProps } from '@/components/props';
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
import Overlay from '@/components/ui/Overlay';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import DlgGraphParams from '@/dialogs/DlgGraphParams';
import useLocalStorage from '@/hooks/useLocalStorage';
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
import { isBasicConcept } from '@/models/rsformAPI';
import { useMainHeight } from '@/stores/appLayout';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { PARAMETER, storage } from '@/utils/constants';
import { errors } from '@/utils/labels';
@ -53,7 +53,7 @@ interface TGFlowProps {
}
function TGFlow({ onOpenEdit }: TGFlowProps) {
const { mainHeight } = useConceptOptions();
const mainHeight = useMainHeight();
const controller = useRSEdit();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);

View File

@ -11,6 +11,7 @@ import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { GraphColoring } from '@/models/miscellaneous';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { useFitHeight } from '@/stores/appLayout';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { globals, PARAMETER, prefixes, storage } from '@/utils/constants';
@ -26,11 +27,11 @@ interface ViewHiddenProps {
}
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize();
const localSelected = items.filter(id => selected.includes(id));
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
const { setHoverCst } = useConceptOptions();
const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
function handleClick(cstID: ConstituentaID, event: CProps.EventMouse) {
if (event.ctrlKey || event.metaKey) {
@ -77,7 +78,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
'cc-scroll-y'
)}
style={{
maxHeight: calculateHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px'),
maxHeight: hiddenHeight,
transitionProperty: 'clip-path',
transitionDuration: `${PARAMETER.fastAnimation}ms`,
transitionTimingFunction: 'ease-out',

View File

@ -7,7 +7,6 @@ import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
@ -51,6 +50,7 @@ import {
} from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI';
import { UserID, UserLevel } from '@/models/user';
import { usePreferencesStore } from '@/stores/preferences';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
@ -142,7 +142,7 @@ export const RSEditState = ({
}: React.PropsWithChildren<RSEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const { adminMode } = useConceptOptions();
const adminMode = usePreferencesStore(state => state.adminMode);
const { accessLevel, setAccessLevel } = useAccessMode();
const model = useRSForm();

View File

@ -13,13 +13,13 @@ import Loader from '@/components/ui/Loader';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useGlobalOss } from '@/context/GlobalOssContext';
import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelVersion, prompts } from '@/utils/labels';
@ -45,7 +45,7 @@ function RSTabs() {
const version = query.get('v') ? Number(query.get('v')) : undefined;
const cstQuery = query.get('active');
const { setNoFooter } = useConceptOptions();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const library = useLibrary();
const oss = useGlobalOss();
@ -73,7 +73,7 @@ function RSTabs() {
}, [schema, schema?.title]);
useEffect(() => {
setNoFooter(activeTab !== RSTabID.CARD);
hideFooter(activeTab !== RSTabID.CARD);
setIsModified(false);
if (activeTab === RSTabID.CST_EDIT) {
const cstID = Number(cstQuery);
@ -83,8 +83,8 @@ function RSTabs() {
setSelected([]);
}
}
return () => setNoFooter(false);
}, [activeTab, cstQuery, setSelected, schema, setNoFooter, setIsModified]);
return () => hideFooter(false);
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
function navigateTab(tab: RSTabID, activeID?: ConstituentaID) {
if (!schema) {

View File

@ -4,10 +4,10 @@ import clsx from 'clsx';
import { useState } from 'react';
import { useAccessMode } from '@/context/AccessModeContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { UserLevel } from '@/models/user';
import { useFitHeight } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants';
import ConstituentsSearch from './ConstituentsSearch';
@ -26,9 +26,9 @@ interface ViewConstituentsProps {
}
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) {
const { calculateHeight } = useConceptOptions();
const windowSize = useWindowSize();
const { accessLevel } = useAccessMode();
const listHeight = useFitHeight(!isBottom ? '8.2rem' : accessLevel !== UserLevel.READER ? '42rem' : '35rem', '10rem');
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
@ -57,11 +57,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
setFiltered={setFilteredData}
/>
<TableSideConstituents
maxHeight={
isBottom
? calculateHeight(accessLevel !== UserLevel.READER ? '42rem' : '35rem', '10rem')
: calculateHeight('8.2rem')
}
maxHeight={listHeight}
items={filteredData}
activeCst={activeCst}
onOpenEdit={onOpenEdit}

View File

@ -0,0 +1,68 @@
import { create } from 'zustand';
import { PARAMETER } from '@/utils/constants';
/** Application layout state manager. */
interface AppLayoutStore {
noNavigation: boolean;
noNavigationAnimation: boolean;
toggleNoNavigation: () => void;
noFooter: boolean;
hideFooter: (value?: boolean) => void;
noScroll: boolean;
hideScroll: (value?: boolean) => void;
}
export const useAppLayoutStore = create<AppLayoutStore>()(set => ({
noNavigation: false,
noNavigationAnimation: false,
toggleNoNavigation: () =>
set(state => {
if (state.noNavigation) {
return { noNavigation: false, noNavigationAnimation: false };
} else {
setTimeout(() => set({ noNavigation: true, noNavigationAnimation: true }), PARAMETER.moveDuration);
return { noNavigation: false, noNavigationAnimation: true };
}
}),
noFooter: false,
hideFooter: value => set({ noFooter: value ?? true }),
noScroll: true,
hideScroll: value => set({ noScroll: value ?? true })
}));
/** Utility function that returns the height of the main area. */
export function useMainHeight(): string {
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter);
if (noNavigation) {
return '100dvh';
} else if (noFooter) {
return 'calc(100dvh - 3rem)';
} else {
return 'calc(100dvh - 6.75rem)';
}
}
/** Utility function that returns the height of the viewport. */
export function useViewportHeight(): string {
const noNavigation = useAppLayoutStore(state => state.noNavigation);
return !noNavigation ? 'calc(100dvh - 3rem)' : '100dvh';
}
/** Utility function that returns the height of the viewport with a given offset. */
export function useFitHeight(offset: string, minimum: string = '0px'): string {
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter);
if (noNavigation) {
return `max(calc(100dvh - (${offset})), ${minimum})`;
} else if (noFooter) {
return `max(calc(100dvh - 3rem - (${offset})), ${minimum})`;
} else {
return `max(calc(100dvh - 6.75rem - (${offset})), ${minimum})`;
}
}

View File

@ -0,0 +1,24 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface PreferencesStore {
showHelp: boolean;
adminMode: boolean;
toggleShowHelp: () => void;
toggleAdminMode: () => void;
}
export const usePreferencesStore = create<PreferencesStore>()(
persist(
set => ({
showHelp: true,
adminMode: false,
toggleShowHelp: () => set(state => ({ showHelp: !state.showHelp })),
toggleAdminMode: () => set(state => ({ adminMode: !state.adminMode }))
}),
{
version: 1,
name: 'portal.preferences'
}
)
);

View File

@ -109,8 +109,6 @@ export const storage = {
PREFIX: 'portal.',
themeDark: 'theme.dark',
optionsAdmin: 'options.admin',
optionsHelp: 'options.help',
rseditShowList: 'rsedit.show_list',
rseditShowControls: 'rsedit.show_controls',