Refactor navigation

This commit is contained in:
IRBorisov 2023-09-05 00:23:53 +03:00
parent 895ad1554b
commit b53fe27b1e
22 changed files with 190 additions and 98 deletions

View File

@ -3,6 +3,7 @@ import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom';
import ConceptToaster from './components/ConceptToaster'; import ConceptToaster from './components/ConceptToaster';
import Footer from './components/Footer'; import Footer from './components/Footer';
import Navigation from './components/Navigation/Navigation'; import Navigation from './components/Navigation/Navigation';
import { NavigationState } from './context/NagivationContext';
import { useConceptTheme } from './context/ThemeContext'; import { useConceptTheme } from './context/ThemeContext';
import CreateRSFormPage from './pages/CreateRSFormPage'; import CreateRSFormPage from './pages/CreateRSFormPage';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
@ -14,25 +15,36 @@ import RegisterPage from './pages/RegisterPage';
import RestorePasswordPage from './pages/RestorePasswordPage'; import RestorePasswordPage from './pages/RestorePasswordPage';
import RSFormPage from './pages/RSFormPage'; import RSFormPage from './pages/RSFormPage';
import UserProfilePage from './pages/UserProfilePage'; import UserProfilePage from './pages/UserProfilePage';
import { globalIDs } from './utils/constants';
function Root() { function Root() {
const { noNavigation, noFooter, viewportHeight, mainHeight } = useConceptTheme(); const { noNavigation, noFooter, viewportHeight, mainHeight, showScroll } = useConceptTheme();
return ( return (
<NavigationState>
<div className='w-screen antialiased clr-app'> <div className='w-screen antialiased clr-app'>
<Navigation />
<ConceptToaster <ConceptToaster
className='mt-[4rem] text-sm' className='mt-[4rem] text-sm'
autoClose={3000} autoClose={3000}
draggable={false} draggable={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
/> />
<div className='w-full overflow-auto' style={{maxHeight: viewportHeight}}>
<main className='w-full h-full' style={{minHeight: mainHeight}}> <Navigation />
<Outlet /> <div id={globalIDs.main_scroll}
</main> className='w-full overflow-x-auto'
{!noNavigation && !noFooter && <Footer />} style={{
maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto'
}}
>
<main className='w-full h-full' style={{minHeight: mainHeight}}>
<Outlet />
</main>
{!noNavigation && !noFooter && <Footer />}
</div> </div>
</div> </div>
</NavigationState>
); );
} }

View File

@ -6,17 +6,17 @@ function HelpMain() {
return ( return (
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<h1>Портал</h1> <h1>Портал</h1>
<p className='lg:text-justify'>Портал позволяет анализировать предметные области, формально записывать системы определений (концептуальные схемы) и синтезировать их с помощью математического аппарата родов структур.</p> <p className=''>Портал позволяет анализировать предметные области, формально записывать системы определений (концептуальные схемы) и синтезировать их с помощью математического аппарата родов структур.</p>
<p className='mt-2 lg:text-justify'>Навигация по порталу осуществляется верхнюю панель или ссылки в "подвале" страницы. Их можно скрыть с помощью кнопки в правом верхнем углу.</p> <p className='mt-2'>Навигация по порталу осуществляется верхнюю панель или ссылки в "подвале" страницы. Их можно скрыть с помощью кнопки в правом верхнем углу.</p>
<p className='mt-2 lg:text-justify'>В меню пользователя (правый верхний угол) доступно редактирование данных пользователя и изменение цветовой темы.</p> <p className='mt-2'>В меню пользователя (правый верхний угол) доступно редактирование данных пользователя и изменение цветовой темы.</p>
<p className='mt-4 mb-1 text-center'><b>Основные разделы</b></p> <p className='mt-4 mb-1 text-center'><b>Основные разделы</b></p>
<li className='text-left'><TextURL text='Библиотека' href='/library' /> - все схемы доступные пользователю</li> <li className='text-left'><TextURL text='Библиотека' href='/library' /> - все схемы доступные пользователю</li>
<li className='text-left'><TextURL text='Общие схемы' href={`/library?filter=${LibraryFilterStrategy.COMMON}`} /> - общедоступные схемы и инструменты поиска и навигации по ним</li> <li className='text-left'><TextURL text='Общие схемы' href={`/library?filter=${LibraryFilterStrategy.COMMON}`} /> - общедоступные схемы и инструменты поиска и навигации по ним</li>
<li className='text-left'><TextURL text='Мои схемы' href={`/library?filter=${LibraryFilterStrategy.PERSONAL}`} /> - отслеживаемые и редактируемые схемы. Основной рабочий раздел</li> <li className='text-left'><TextURL text='Мои схемы' href={`/library?filter=${LibraryFilterStrategy.PERSONAL}`} /> - отслеживаемые и редактируемые схемы. Основной рабочий раздел</li>
<li className='text-left'><TextURL text='Профиль' href='/profile' /> - данные пользователя и смена пароля</li> <li className='text-left'><TextURL text='Профиль' href='/profile' /> - данные пользователя и смена пароля</li>
<p className='mt-4 mb-1 text-center'><b>Поддержка</b></p> <p className='mt-4 mb-1 text-center'><b>Поддержка</b></p>
<p className='lg:text-justify'>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.gitrepo}/>.</p> <p className=''>Портал разрабатывается <TextURL text='Центром Концепт' href={urls.concept}/> и является проектом с открытым исходным кодом, доступным на <TextURL text='Github' href={urls.gitrepo}/>.</p>
<p className='mt-2 lg:text-justify'>Ждём Ваши пожелания по доработке, найденные ошибки и иные предложения по адресу <TextURL href={urls.mailportal} text='portal@acconcept.ru'/></p> <p className='mt-2'>Ждём Ваши пожелания по доработке, найденные ошибки и иные предложения по адресу <TextURL href={urls.mailportal} text='portal@acconcept.ru'/></p>
<p></p> <p></p>
</div> </div>
); );

View File

@ -1,5 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { useConceptNavigation } from '../../context/NagivationContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { EducationIcon, LibraryIcon, PlusIcon } from '../Icons'; import { EducationIcon, LibraryIcon, PlusIcon } from '../Icons';
import Logo from './Logo' import Logo from './Logo'
@ -7,12 +6,12 @@ import NavigationButton from './NavigationButton';
import UserMenu from './UserMenu'; import UserMenu from './UserMenu';
function Navigation () { function Navigation () {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const { noNavigation, toggleNoNavigation } = useConceptTheme(); const { noNavigation, toggleNoNavigation } = useConceptTheme();
const navigateLibrary = () => navigate('/library'); const navigateLibrary = () => navigateTo('/library');
const navigateHelp = () => navigate('/manuals'); const navigateHelp = () => navigateTo('/manuals');
const navigateCreateNew = () => navigate('/rsform-create'); const navigateCreateNew = () => navigateTo('/rsform-create');
return ( return (
<nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'> <nav className='sticky top-0 left-0 right-0 z-50 select-none h-fit'>

View File

@ -1,6 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { LibraryFilterStrategy } from '../../utils/models'; import { LibraryFilterStrategy } from '../../utils/models';
import Dropdown from '../Common/Dropdown'; import Dropdown from '../Common/Dropdown';
@ -12,23 +11,23 @@ interface UserDropdownProps {
function UserDropdown({ hideDropdown }: UserDropdownProps) { function UserDropdown({ hideDropdown }: UserDropdownProps) {
const { darkMode, toggleDarkMode } = useConceptTheme(); const { darkMode, toggleDarkMode } = useConceptTheme();
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navigateProfile = () => { const navigateProfile = () => {
hideDropdown(); hideDropdown();
navigate('/profile'); navigateTo('/profile');
}; };
const logoutAndRedirect = const logoutAndRedirect =
() => { () => {
hideDropdown(); hideDropdown();
logout(() => navigate('/login/')); logout(() => navigateTo('/login/'));
}; };
const navigateMyWork = () => { const navigateMyWork = () => {
hideDropdown(); hideDropdown();
navigate(`/library?filter=${LibraryFilterStrategy.PERSONAL}`); navigateTo(`/library?filter=${LibraryFilterStrategy.PERSONAL}`);
}; };
return ( return (

View File

@ -1,17 +1,16 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import useDropdown from '../../hooks/useDropdown'; import useDropdown from '../../hooks/useDropdown';
import { InDoor, UserIcon } from '../Icons'; import { InDoor, UserIcon } from '../Icons';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
import UserDropdown from './UserDropdown'; import UserDropdown from './UserDropdown';
function UserMenu() { function UserMenu() {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const menu = useDropdown(); const menu = useDropdown();
const navigateLogin = () => navigate('/login'); const navigateLogin = () => navigateTo('/login');
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
<div className='flex items-center justify-end h-full w-fit'> <div className='flex items-center justify-end h-full w-fit'>

View File

@ -0,0 +1,60 @@
import { createContext, useCallback, useContext, useEffect } from 'react';
import { NavigateOptions, useLocation, useNavigate } from 'react-router-dom';
import { globalIDs } from '../utils/constants';
interface INagivationContext{
navigateTo: (path: string, options?: NavigateOptions) => void
navigateHistory: (offset: number) => void
}
const NagivationContext = createContext<INagivationContext | null>(null);
export const useConceptNavigation = () => {
const context = useContext(NagivationContext);
if (!context) {
throw new Error('useConceptNavigation has to be used within <NavigationState.Provider>');
}
return context;
}
interface NavigationStateProps {
children: React.ReactNode
}
export const NavigationState = ({ children }: NavigationStateProps) => {
const implNavigate = useNavigate();
const { pathname } = useLocation();
function scrollTop() {
window.scrollTo(0, 0);
const mainScroll = document.getElementById(globalIDs.main_scroll);
if (mainScroll) {
mainScroll.scroll(0,0);
}
}
const navigateTo = useCallback(
(path: string, options?: NavigateOptions) => {
scrollTop();
implNavigate(path, options);
}, [implNavigate]);
const navigateHistory = useCallback(
(offset: number) => {
scrollTop();
implNavigate(offset);
}, [implNavigate]);
useEffect(() => {
scrollTop();
}, [pathname]);
return (
<NagivationContext.Provider value={{
navigateTo, navigateHistory
}}>
{children}
</NagivationContext.Provider>
);
}

View File

@ -13,9 +13,13 @@ interface IThemeContext {
toggleDarkMode: () => void toggleDarkMode: () => void
noNavigation: boolean noNavigation: boolean
toggleNoNavigation: () => void
noFooter: boolean noFooter: boolean
setNoFooter: (value: boolean) => void setNoFooter: (value: boolean) => void
toggleNoNavigation: () => void
showScroll: boolean
setShowScroll: (value: boolean) => void
} }
const ThemeContext = createContext<IThemeContext | null>(null); const ThemeContext = createContext<IThemeContext | null>(null);
@ -36,6 +40,7 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
const [colors, setColors] = useState<IColorTheme>(lightT); const [colors, setColors] = useState<IColorTheme>(lightT);
const [noNavigation, setNoNavigation] = useState(false); const [noNavigation, setNoNavigation] = useState(false);
const [noFooter, setNoFooter] = useState(false); const [noFooter, setNoFooter] = useState(false);
const [showScroll, setShowScroll] = useState(false);
function setDarkClass(isDark: boolean) { function setDarkClass(isDark: boolean) {
const root = window.document.documentElement; const root = window.document.documentElement;
@ -72,10 +77,10 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
return ( return (
<ThemeContext.Provider value={{ <ThemeContext.Provider value={{
darkMode, colors, darkMode, colors,
noNavigation, noFooter, noNavigation, noFooter, showScroll,
toggleDarkMode: () => setDarkMode(prev => !prev), toggleDarkMode: () => setDarkMode(prev => !prev),
toggleNoNavigation: () => setNoNavigation(prev => !prev), toggleNoNavigation: () => setNoNavigation(prev => !prev),
setNoFooter, setNoFooter, setShowScroll,
viewportHeight, mainHeight viewportHeight, mainHeight
}}> }}>
{children} {children}

View File

@ -153,7 +153,7 @@
} }
:is(.clr-selected, :is(.clr-selected,
.clr-btn-primary, .clr-btn-primary
) { ) {
color: var(--cl-fg-100); color: var(--cl-fg-100);
background-color: var(--cl-prim-bg-80); background-color: var(--cl-prim-bg-80);

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
@ -12,11 +12,12 @@ import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import RequireAuth from '../components/RequireAuth'; import RequireAuth from '../components/RequireAuth';
import { useLibrary } from '../context/LibraryContext'; import { useLibrary } from '../context/LibraryContext';
import { useConceptNavigation } from '../context/NagivationContext';
import { IRSFormCreateData, LibraryItemType } from '../utils/models'; import { IRSFormCreateData, LibraryItemType } from '../utils/models';
function CreateRSFormPage() { function CreateRSFormPage() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const { navigateTo, navigateHistory } = useConceptNavigation();
const { createSchema, error, setError, processing } = useLibrary(); const { createSchema, error, setError, processing } = useLibrary();
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
@ -39,9 +40,9 @@ function CreateRSFormPage() {
function handleCancel() { function handleCancel() {
if (location.key !== 'default') { if (location.key !== 'default') {
navigate(-1); navigateHistory(-1);
} else { } else {
navigate('/library'); navigateTo('/library');
} }
} }
@ -62,7 +63,7 @@ function CreateRSFormPage() {
}; };
createSchema(data, (newSchema) => { createSchema(data, (newSchema) => {
toast.success('Схема успешно создана'); toast.success('Схема успешно создана');
navigate(`/rsforms/${newSchema.id}`); navigateTo(`/rsforms/${newSchema.id}`);
}); });
} }

View File

@ -1,24 +1,24 @@
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext';
import { TIMEOUT_UI_REFRESH } from '../utils/constants'; import { TIMEOUT_UI_REFRESH } from '../utils/constants';
function HomePage() { function HomePage() {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
useLayoutEffect(() => { useLayoutEffect(() => {
if (!user) { if (!user) {
setTimeout(() => { setTimeout(() => {
navigate('/manuals'); navigateTo('/manuals');
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} else { } else {
setTimeout(() => { setTimeout(() => {
navigate('/library'); navigateTo('/library');
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} }
}, [navigate, user]) }, [navigateTo, user])
return ( return (
<div className='flex flex-col items-center justify-center w-full px-4 py-2'> <div className='flex flex-col items-center justify-center w-full px-4 py-2'>

View File

@ -1,8 +1,9 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { MagnifyingGlassIcon } from '../../components/Icons'; import { MagnifyingGlassIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models'; import { ILibraryFilter, LibraryFilterStrategy } from '../../utils/models';
import PickerStrategy from './PickerStrategy'; import PickerStrategy from './PickerStrategy';
@ -26,7 +27,7 @@ interface SearchPanelProps {
} }
function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) { function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const search = useLocation().search; const search = useLocation().search;
const { user } = useAuth(); const { user } = useAuth();
@ -51,29 +52,29 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
useLayoutEffect(() => { useLayoutEffect(() => {
const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null; const searchFilter = new URLSearchParams(search).get('filter') as LibraryFilterStrategy | null;
if (searchFilter === null) { if (searchFilter === null) {
navigate(`/library?filter=${strategy}`, { replace: true }); navigateTo(`/library?filter=${strategy}`, { replace: true });
return; return;
} }
const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL; const inputStrategy = searchFilter && Object.values(LibraryFilterStrategy).includes(searchFilter) ? searchFilter : LibraryFilterStrategy.MANUAL;
setQuery('') setQuery('')
setStrategy(inputStrategy) setStrategy(inputStrategy)
setFilter(ApplyStrategy(inputStrategy)); setFilter(ApplyStrategy(inputStrategy));
}, [user, search, setQuery, setFilter, setStrategy, strategy, navigate]); }, [user, search, setQuery, setFilter, setStrategy, strategy, navigateTo]);
const handleChangeStrategy = useCallback( const handleChangeStrategy = useCallback(
(value: LibraryFilterStrategy) => { (value: LibraryFilterStrategy) => {
if (value === strategy) { if (value === strategy) {
return; return;
} }
navigate(`/library?filter=${value}`) navigateTo(`/library?filter=${value}`)
}, [strategy, navigate]); }, [strategy, navigateTo]);
return ( return (
<div className='sticky top-0 left-0 right-0 z-30 flex items-center justify-start w-full border-b clr-input'> <div className='sticky top-0 left-0 right-0 z-30 flex items-center justify-start w-full border-b clr-input'>
<div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'> <div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'>
Фильтр Фильтр
<span className='ml-2'> <span className='ml-2'>
<b>{filtered}</b> из {total} {filtered} из {total}
</span> </span>
</div> </div>
<div className='flex items-center justify-center w-full pr-[10rem]'> <div className='flex items-center justify-center w-full pr-[10rem]'>

View File

@ -1,6 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import ConceptDataTable from '../../components/Common/ConceptDataTable'; import ConceptDataTable from '../../components/Common/ConceptDataTable';
import ConceptTooltip from '../../components/Common/ConceptTooltip'; import ConceptTooltip from '../../components/Common/ConceptTooltip';
@ -8,6 +7,7 @@ import TextURL from '../../components/Common/TextURL';
import HelpLibrary from '../../components/Help/HelpLibrary'; import HelpLibrary from '../../components/Help/HelpLibrary';
import { EducationIcon, EyeIcon, GroupIcon, HelpIcon } from '../../components/Icons'; import { EducationIcon, EyeIcon, GroupIcon, HelpIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import { useUsers } from '../../context/UsersContext'; import { useUsers } from '../../context/UsersContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { ILibraryItem } from '../../utils/models'; import { ILibraryItem } from '../../utils/models';
@ -18,12 +18,12 @@ interface ViewLibraryProps {
} }
function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) { function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
const { user } = useAuth(); const { navigateTo } = useConceptNavigation();
const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
const { user } = useAuth();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`); const openRSForm = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
const columns = useMemo( const columns = useMemo(
() => [ () => [

View File

@ -3,16 +3,24 @@ import { useLayoutEffect, useState } from 'react';
import BackendError from '../../components/BackendError' import BackendError from '../../components/BackendError'
import { ConceptLoader } from '../../components/Common/ConceptLoader' import { ConceptLoader } from '../../components/Common/ConceptLoader'
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { ILibraryFilter, ILibraryItem } from '../../utils/models'; import { ILibraryFilter, ILibraryItem } from '../../utils/models';
import SearchPanel from './SearchPanel'; import SearchPanel from './SearchPanel';
import ViewLibrary from './ViewLibrary'; import ViewLibrary from './ViewLibrary';
function LibraryPage() { function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const { setShowScroll } = useConceptTheme();
const [ filter, setFilter ] = useState<ILibraryFilter>({}); const [ filter, setFilter ] = useState<ILibraryFilter>({});
const [ items, setItems ] = useState<ILibraryItem[]>([]); const [ items, setItems ] = useState<ILibraryItem[]>([]);
useLayoutEffect(
() => {
setShowScroll(true);
return () => setShowScroll(false);
}, [setShowScroll]);
useLayoutEffect( useLayoutEffect(
() => { () => {
setItems(library.filter(filter)); setItems(library.filter(filter));

View File

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import BackendError, { ErrorInfo } from '../components/BackendError'; import BackendError, { ErrorInfo } from '../components/BackendError';
import Form from '../components/Common/Form'; import Form from '../components/Common/Form';
@ -8,6 +8,7 @@ import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import TextURL from '../components/Common/TextURL'; import TextURL from '../components/Common/TextURL';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext';
import { useConceptTheme } from '../context/ThemeContext'; import { useConceptTheme } from '../context/ThemeContext';
import { IUserLoginData } from '../utils/models'; import { IUserLoginData } from '../utils/models';
@ -26,7 +27,7 @@ function ProcessError({error}: {error: ErrorInfo}): React.ReactElement {
function LoginPage() { function LoginPage() {
const {mainHeight} = useConceptTheme(); const {mainHeight} = useConceptTheme();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const { navigateTo, navigateHistory } = useConceptNavigation();
const search = useLocation().search; const search = useLocation().search;
const { user, login, loading, error, setError } = useAuth(); const { user, login, loading, error, setError } = useAuth();
@ -52,9 +53,9 @@ function LoginPage() {
}; };
login(data, () => { login(data, () => {
if (location.key !== 'default') { if (location.key !== 'default') {
navigate(-1); navigateHistory(-1);
} else { } else {
navigate('/library'); navigateTo('/library');
} }
}); });
} }

View File

@ -1,36 +1,37 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useConceptNavigation } from '../../context/NagivationContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { HelpTopic } from '../../utils/models'; import { HelpTopic } from '../../utils/models';
import TopicsList from './TopicsList'; import TopicsList from './TopicsList';
import ViewTopic from './ViewTopic'; import ViewTopic from './ViewTopic';
function ManualsPage() { function ManualsPage() {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const search = useLocation().search; const search = useLocation().search;
const { mainHeight } = useConceptTheme(); const { mainHeight } = useConceptTheme();
const [activeTopic, setActiveTopic] = useState<HelpTopic>(HelpTopic.MAIN); const [activeTopic, setActiveTopic] = useState<HelpTopic>(HelpTopic.MAIN);
const navigateTo = useCallback( const navigateTopic = useCallback(
(newTopic: HelpTopic) => { (newTopic: HelpTopic) => {
navigate(`/manuals?topic=${newTopic}`); navigateTo(`/manuals?topic=${newTopic}`);
}, [navigate]); }, [navigateTo]);
function onSelectTopic(newTopic: HelpTopic) { function onSelectTopic(newTopic: HelpTopic) {
navigateTo(newTopic); navigateTopic(newTopic);
} }
useLayoutEffect(() => { useLayoutEffect(() => {
const topic = new URLSearchParams(search).get('topic') as HelpTopic; const topic = new URLSearchParams(search).get('topic') as HelpTopic;
if (!Object.values(HelpTopic).includes(topic)) { if (!Object.values(HelpTopic).includes(topic)) {
navigateTo(HelpTopic.MAIN); navigateTopic(HelpTopic.MAIN);
return; return;
} else { } else {
setActiveTopic(topic); setActiveTopic(topic);
} }
}, [search, setActiveTopic, navigateTo]); }, [search, setActiveTopic, navigateTopic]);
return ( return (
<div className='flex w-full gap-2 justify-stretch' style={{minHeight: mainHeight}}> <div className='flex w-full gap-2 justify-stretch' style={{minHeight: mainHeight}}>

View File

@ -1,5 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import Checkbox from '../../components/Common/Checkbox'; import Checkbox from '../../components/Common/Checkbox';
@ -7,6 +6,7 @@ import Modal from '../../components/Common/Modal';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { IRSFormCreateData } from '../../utils/models'; import { IRSFormCreateData } from '../../utils/models';
import { getCloneTitle } from '../../utils/staticUI'; import { getCloneTitle } from '../../utils/staticUI';
@ -16,7 +16,7 @@ interface DlgCloneRSFormProps {
} }
function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) { function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
@ -50,7 +50,7 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
}; };
cloneSchema(schema.id, data, newSchema => { cloneSchema(schema.id, data, newSchema => {
toast.success(`Схема создана: ${newSchema.alias}`); toast.success(`Схема создана: ${newSchema.alias}`);
navigate(`/rsforms/${newSchema.id}`); navigateTo(`/rsforms/${newSchema.id}`);
}); });
}; };

View File

@ -264,7 +264,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
<div className='mr-3 whitespace-nowrap'> <div className='mr-3 whitespace-nowrap'>
Выбраны Выбраны
<span className='ml-2'> <span className='ml-2'>
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0} {selected.length} из {schema?.stats?.count_all ?? 0}
</span> </span>
</div> </div>
<div className='flex items-center justify-start w-full gap-1'> <div className='flex items-center justify-start w-full gap-1'>

View File

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -10,6 +10,7 @@ import { ConceptLoader } from '../../components/Common/ConceptLoader';
import ConceptTab from '../../components/Common/ConceptTab'; import ConceptTab from '../../components/Common/ConceptTab';
import TextURL from '../../components/Common/TextURL'; import TextURL from '../../components/Common/TextURL';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useModificationPrompt from '../../hooks/useModificationPrompt'; import useModificationPrompt from '../../hooks/useModificationPrompt';
@ -50,7 +51,7 @@ function ProcessError({error}: {error: ErrorInfo}): React.ReactElement {
} }
function RSTabs() { function RSTabs() {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const search = useLocation().search; const search = useLocation().search;
const { const {
error, schema, loading, claim, download, isTracking, error, schema, loading, claim, download, isTracking,
@ -101,25 +102,25 @@ function RSTabs() {
}, [search, setActiveTab, setActiveID, schema, setNoFooter]); }, [search, setActiveTab, setActiveID, schema, setNoFooter]);
function onSelectTab(index: number) { function onSelectTab(index: number) {
navigateTo(index, activeID); navigateTab(index, activeID);
} }
const navigateTo = useCallback( const navigateTab = useCallback(
(tab: RSTabID, activeID?: number) => { (tab: RSTabID, activeID?: number) => {
if (!schema) { if (!schema) {
return; return;
} }
if (activeID) { if (activeID) {
navigate(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, { navigateTo(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, {
replace: tab === activeTab && tab !== RSTabID.CST_EDIT replace: tab === activeTab && tab !== RSTabID.CST_EDIT
}); });
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) { } else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
activeID = schema.items[0].id; activeID = schema.items[0].id;
navigate(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, { replace: true }); navigateTo(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`, { replace: true });
} else { } else {
navigate(`/rsforms/${schema.id}?tab=${tab}`); navigateTo(`/rsforms/${schema.id}?tab=${tab}`);
} }
}, [navigate, schema, activeTab]); }, [navigateTo, schema, activeTab]);
const handleCreateCst = useCallback( const handleCreateCst = useCallback(
(data: ICstCreateData) => { (data: ICstCreateData) => {
@ -129,7 +130,7 @@ function RSTabs() {
data.alias = createAliasFor(data.cst_type, schema); data.alias = createAliasFor(data.cst_type, schema);
cstCreate(data, newCst => { cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`); toast.success(`Конституента добавлена: ${newCst.alias}`);
navigateTo(activeTab, newCst.id); navigateTab(activeTab, newCst.id);
if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) { if (activeTab === RSTabID.CST_EDIT || activeTab === RSTabID.CST_LIST) {
setTimeout(() => { setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`); const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
@ -143,7 +144,7 @@ function RSTabs() {
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} }
}); });
}, [schema, cstCreate, navigateTo, activeTab]); }, [schema, cstCreate, navigateTab, activeTab]);
const promptCreateCst = useCallback( const promptCreateCst = useCallback(
(initialData: ICstCreateData, skipDialog?: boolean) => { (initialData: ICstCreateData, skipDialog?: boolean) => {
@ -181,7 +182,7 @@ function RSTabs() {
const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', '); const deletedNames = deleted.map(id => schema.items.find(cst => cst.id === id)?.alias).join(', ');
toast.success(`Конституенты удалены: ${deletedNames}`); toast.success(`Конституенты удалены: ${deletedNames}`);
if (deleted.length === schema.items.length) { if (deleted.length === schema.items.length) {
navigateTo(RSTabID.CST_LIST); navigateTab(RSTabID.CST_LIST);
} }
if (activeIndex) { if (activeIndex) {
while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) { while (activeIndex < schema.items.length && deleted.find(id => id === schema.items[activeIndex].id)) {
@ -193,11 +194,11 @@ function RSTabs() {
--activeIndex; --activeIndex;
} }
} }
navigateTo(activeTab, schema.items[activeIndex].id); navigateTab(activeTab, schema.items[activeIndex].id);
} }
if (afterDelete) afterDelete(deleted); if (afterDelete) afterDelete(deleted);
}); });
}, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTo]); }, [afterDelete, cstDelete, schema, activeID, activeTab, navigateTab]);
const promptDeleteCst = useCallback( const promptDeleteCst = useCallback(
(selected: number[], callback?: (items: number[]) => void) => { (selected: number[], callback?: (items: number[]) => void) => {
@ -218,8 +219,8 @@ function RSTabs() {
const onOpenCst = useCallback( const onOpenCst = useCallback(
(cstID: number) => { (cstID: number) => {
navigateTo(RSTabID.CST_EDIT, cstID) navigateTab(RSTabID.CST_EDIT, cstID)
}, [navigateTo]); }, [navigateTab]);
const onDestroySchema = useCallback( const onDestroySchema = useCallback(
() => { () => {
@ -228,9 +229,9 @@ function RSTabs() {
} }
destroySchema(schema.id, () => { destroySchema(schema.id, () => {
toast.success('Схема удалена'); toast.success('Схема удалена');
navigate('/library'); navigateTo('/library');
}); });
}, [schema, destroySchema, navigate]); }, [schema, destroySchema, navigateTo]);
const onClaimSchema = useCallback( const onClaimSchema = useCallback(
() => { () => {

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
@ -8,11 +8,12 @@ import Form from '../components/Common/Form';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useConceptNavigation } from '../context/NagivationContext';
import { type IUserSignupData } from '../utils/models'; import { type IUserSignupData } from '../utils/models';
function RegisterPage() { function RegisterPage() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const { navigateTo, navigateHistory } = useConceptNavigation();
const { user, signup, loading, error, setError } = useAuth(); const { user, signup, loading, error, setError } = useAuth();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -28,9 +29,9 @@ function RegisterPage() {
function handleCancel() { function handleCancel() {
if (location.key !== 'default') { if (location.key !== 'default') {
navigate(-1); navigateHistory(-1);
} else { } else {
navigate('/library'); navigateTo('/library');
} }
} }
@ -46,7 +47,7 @@ function RegisterPage() {
last_name: lastName last_name: lastName
}; };
signup(data, createdUser => { signup(data, createdUser => {
navigate(`/login?username=${createdUser.username}`); navigateTo(`/login?username=${createdUser.username}`);
toast.success(`Пользователь успешно создан: ${createdUser.username}`); toast.success(`Пользователь успешно создан: ${createdUser.username}`);
}); });
} }

View File

@ -1,21 +1,21 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext';
import { IUserUpdatePassword } from '../../utils/models'; import { IUserUpdatePassword } from '../../utils/models';
function EditorPassword() { function EditorPassword() {
const { navigateTo } = useConceptNavigation();
const { updatePassword, error, setError, loading } = useAuth(); const { updatePassword, error, setError, loading } = useAuth();
const [oldPassword, setOldPassword] = useState(''); const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [newPasswordRepeat, setNewPasswordRepeat] = useState(''); const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
const navigate = useNavigate();
const passwordColor = useMemo( const passwordColor = useMemo(
() => { () => {
@ -39,7 +39,7 @@ function EditorPassword() {
}; };
updatePassword(data, () => { updatePassword(data, () => {
toast.success('Изменения сохранены'); toast.success('Изменения сохранены');
navigate('/login') navigateTo('/login');
}); });
} }

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import ConceptDataTable from '../../components/Common/ConceptDataTable'; import ConceptDataTable from '../../components/Common/ConceptDataTable';
import { useConceptNavigation } from '../../context/NagivationContext';
import { ILibraryItem } from '../../utils/models'; import { ILibraryItem } from '../../utils/models';
interface ViewSubscriptionsProps { interface ViewSubscriptionsProps {
@ -10,10 +10,10 @@ interface ViewSubscriptionsProps {
} }
function ViewSubscriptions({items}: ViewSubscriptionsProps) { function ViewSubscriptions({items}: ViewSubscriptionsProps) {
const navigate = useNavigate(); const { navigateTo } = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
const openRSForm = (item: ILibraryItem) => navigate(`/rsforms/${item.id}`); const openRSForm = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
const columns = useMemo(() => const columns = useMemo(() =>
[ [

View File

@ -25,6 +25,10 @@ export const resources = {
graph_font: '/DejaVu.ttf' graph_font: '/DejaVu.ttf'
} }
export const globalIDs = {
main_scroll: 'main-scroll'
}
export const prefixes = { export const prefixes = {
cst_list: 'cst-list-', cst_list: 'cst-list-',
cst_status_list: 'cst-status-list-', cst_status_list: 'cst-status-list-',