mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
R: Migrating to zustand for local state management pt2
This commit is contained in:
parent
4c9b0b28b8
commit
8c3e5dfd4d
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -119,6 +119,7 @@
|
||||||
"NUMR",
|
"NUMR",
|
||||||
"Opencorpora",
|
"Opencorpora",
|
||||||
"overscroll",
|
"overscroll",
|
||||||
|
"partialize",
|
||||||
"passwordreset",
|
"passwordreset",
|
||||||
"perfectivity",
|
"perfectivity",
|
||||||
"PNCT",
|
"PNCT",
|
||||||
|
|
|
@ -20,22 +20,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Концепт Портал</title>
|
<title>Концепт Портал</title>
|
||||||
|
|
||||||
<!-- <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script> -->
|
|
||||||
<script>
|
|
||||||
let isDark = false;
|
|
||||||
if ('portal.theme.dark' in localStorage) {
|
|
||||||
isDark = localStorage.getItem('portal.theme.dark') === 'true';
|
|
||||||
} else if (window.matchMedia) {
|
|
||||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
localStorage.setItem('portal.theme.dark', isDark ? 'true' : 'false');
|
|
||||||
}
|
|
||||||
if (isDark) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
|
||||||
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
|
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
|
||||||
|
|
||||||
function ToasterThemed(props: ToasterThemedProps) {
|
function ToasterThemed(props: ToasterThemedProps) {
|
||||||
const { darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
|
||||||
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
|
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
function Logo() {
|
function Logo() {
|
||||||
const { darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globals, PARAMETER } from '@/utils/constants';
|
import { globals, PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
function ToggleNavigation() {
|
function ToggleNavigation() {
|
||||||
const { toggleDarkMode, darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
|
@ -28,10 +27,11 @@ interface UserDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
const { darkMode, toggleDarkMode } = useConceptOptions();
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
||||||
const showHelp = usePreferencesStore(state => state.showHelp);
|
const showHelp = usePreferencesStore(state => state.showHelp);
|
||||||
const toggleShowHelp = usePreferencesStore(state => state.toggleShowHelp);
|
const toggleShowHelp = usePreferencesStore(state => state.toggleShowHelp);
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
|
|
@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
|
||||||
import { forwardRef, useRef } from 'react';
|
import { forwardRef, useRef } from 'react';
|
||||||
|
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
||||||
import { extractGlobals } from '@/models/rslangAPI';
|
import { extractGlobals } from '@/models/rslangAPI';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { APP_COLORS } from '@/styling/color';
|
import { APP_COLORS } from '@/styling/color';
|
||||||
|
|
||||||
import { ccBracketMatching } from './bracketMatching';
|
import { ccBracketMatching } from './bracketMatching';
|
||||||
|
@ -64,7 +64,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
|
||||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
|
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
|
||||||
|
|
|
@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
|
||||||
import { forwardRef, useRef, useState } from 'react';
|
import { forwardRef, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import DlgEditReference from '@/dialogs/DlgEditReference';
|
import DlgEditReference from '@/dialogs/DlgEditReference';
|
||||||
import { ReferenceType } from '@/models/language';
|
import { ReferenceType } from '@/models/language';
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { APP_COLORS } from '@/styling/color';
|
import { APP_COLORS } from '@/styling/color';
|
||||||
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
@ -92,7 +92,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ReactNode } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
|
||||||
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
export type { PlacesType } from 'react-tooltip';
|
export type { PlacesType } from 'react-tooltip';
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ function Tooltip({
|
||||||
style,
|
style,
|
||||||
...restProps
|
...restProps
|
||||||
}: TooltipProps) {
|
}: TooltipProps) {
|
||||||
const { darkMode } = useConceptOptions();
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
|
|
||||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||||
import Loader from '@/components/ui/Loader';
|
import Loader from '@/components/ui/Loader';
|
||||||
import Tooltip from '@/components/ui/Tooltip';
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { globals, PARAMETER, storage } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
interface IOptionsContext {
|
interface IOptionsContext {
|
||||||
darkMode: boolean;
|
|
||||||
toggleDarkMode: () => void;
|
|
||||||
|
|
||||||
folderMode: boolean;
|
|
||||||
setFolderMode: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
|
|
||||||
location: string;
|
|
||||||
setLocation: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
|
|
||||||
setHoverCst: (newValue: IConstituenta | undefined) => void;
|
setHoverCst: (newValue: IConstituenta | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,59 +23,11 @@ export const useConceptOptions = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
||||||
const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false);
|
|
||||||
|
|
||||||
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
|
||||||
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
|
||||||
|
|
||||||
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
|
const [hoverCst, setHoverCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
|
|
||||||
function setDarkClass(isDark: boolean) {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
if (isDark) {
|
|
||||||
root.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
root.classList.remove('dark');
|
|
||||||
}
|
|
||||||
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDarkClass(darkMode);
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
const toggleDarkMode = useCallback(() => {
|
|
||||||
if (!document.startViewTransition) {
|
|
||||||
setDarkMode(prev => !prev);
|
|
||||||
} else {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.innerHTML = `
|
|
||||||
* {
|
|
||||||
animation: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
document.startViewTransition(() => {
|
|
||||||
flushSync(() => {
|
|
||||||
setDarkMode(prev => !prev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => document.head.removeChild(style), PARAMETER.moveDuration);
|
|
||||||
}
|
|
||||||
}, [setDarkMode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsContext
|
<OptionsContext
|
||||||
value={{
|
value={{
|
||||||
darkMode,
|
|
||||||
folderMode,
|
|
||||||
setFolderMode,
|
|
||||||
location,
|
|
||||||
setLocation,
|
|
||||||
toggleDarkMode: toggleDarkMode,
|
|
||||||
setHoverCst
|
setHoverCst
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -20,21 +20,23 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { ILibraryCreateData } from '@/models/library';
|
import { ILibraryCreateData } from '@/models/library';
|
||||||
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
function FormCreateItem() {
|
function FormCreateItem() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const options = useConceptOptions();
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { createItem, processingError, setProcessingError, processing, folders } = useLibrary();
|
const { createItem, processingError, setProcessingError, processing, folders } = useLibrary();
|
||||||
|
|
||||||
|
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||||
|
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
|
||||||
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
|
@ -81,7 +83,7 @@ function FormCreateItem() {
|
||||||
file: file,
|
file: file,
|
||||||
fileName: file?.name
|
fileName: file?.name
|
||||||
};
|
};
|
||||||
options.setLocation(location);
|
setSearchLocation(location);
|
||||||
createItem(data, newItem => {
|
createItem(data, newItem => {
|
||||||
toast.success(information.newLibraryItem);
|
toast.success(information.newLibraryItem);
|
||||||
if (itemType == LibraryItemType.RSFORM) {
|
if (itemType == LibraryItemType.RSFORM) {
|
||||||
|
@ -108,11 +110,11 @@ function FormCreateItem() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!options.location) {
|
if (!searchLocation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleSelectLocation(options.location);
|
handleSelectLocation(searchLocation);
|
||||||
}, [options.location, handleSelectLocation]);
|
}, [searchLocation, handleSelectLocation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (itemType !== LibraryItemType.RSFORM) {
|
if (itemType !== LibraryItemType.RSFORM) {
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import fileDownload from 'js-file-download';
|
import fileDownload from 'js-file-download';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { IconCSV } from '@/components/Icons';
|
import { IconCSV } from '@/components/Icons';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import DataLoader from '@/components/wrap/DataLoader';
|
import DataLoader from '@/components/wrap/DataLoader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
import { IRenameLocationData } from '@/models/library';
|
||||||
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
|
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
|
||||||
import { UserID } from '@/models/user';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { storage } from '@/utils/constants';
|
import { useLibraryFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
|
import { convertToCSV } from '@/utils/utils';
|
||||||
|
|
||||||
import TableLibraryItems from './TableLibraryItems';
|
import TableLibraryItems from './TableLibraryItems';
|
||||||
import ToolbarSearch from './ToolbarSearch';
|
import ToolbarSearch from './ToolbarSearch';
|
||||||
|
@ -27,89 +22,29 @@ import ViewSideLocation from './ViewSideLocation';
|
||||||
|
|
||||||
function LibraryPage() {
|
function LibraryPage() {
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
const { user } = useAuth();
|
|
||||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
|
||||||
const options = useConceptOptions();
|
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
const [path, setPath] = useState('');
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
|
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
|
||||||
|
const filter = useLibraryFilter();
|
||||||
|
const items = library.applyFilter(filter);
|
||||||
|
|
||||||
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
|
||||||
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false);
|
|
||||||
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
|
||||||
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchOwned, undefined);
|
|
||||||
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
|
||||||
const [filterUser, setFilterUser] = useLocalStorage<UserID | undefined>(storage.librarySearchUser, undefined);
|
|
||||||
const [showRenameLocation, setShowRenameLocation] = useState(false);
|
const [showRenameLocation, setShowRenameLocation] = useState(false);
|
||||||
|
|
||||||
const filter: ILibraryFilter = useMemo(
|
|
||||||
() => ({
|
|
||||||
head: head,
|
|
||||||
path: path,
|
|
||||||
query: query,
|
|
||||||
isEditor: user ? isEditor : undefined,
|
|
||||||
isOwned: user ? isOwned : undefined,
|
|
||||||
isVisible: user ? isVisible : true,
|
|
||||||
folderMode: options.folderMode,
|
|
||||||
subfolders: subfolders,
|
|
||||||
location: options.location,
|
|
||||||
filterUser: filterUser
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
head,
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
isEditor,
|
|
||||||
isOwned,
|
|
||||||
isVisible,
|
|
||||||
user,
|
|
||||||
options.folderMode,
|
|
||||||
options.location,
|
|
||||||
subfolders,
|
|
||||||
filterUser
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasCustomFilter =
|
|
||||||
!!filter.path ||
|
|
||||||
!!filter.query ||
|
|
||||||
filter.head !== undefined ||
|
|
||||||
filter.isEditor !== undefined ||
|
|
||||||
filter.isOwned !== undefined ||
|
|
||||||
filter.isVisible !== true ||
|
|
||||||
filter.filterUser !== undefined ||
|
|
||||||
!!filter.location;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setItems(library.applyFilter(filter));
|
|
||||||
}, [library, library.items.length, filter]);
|
|
||||||
|
|
||||||
const toggleFolderMode = () => options.setFolderMode(prev => !prev);
|
|
||||||
|
|
||||||
const resetFilter = useCallback(() => {
|
|
||||||
setQuery('');
|
|
||||||
setPath('');
|
|
||||||
setHead(undefined);
|
|
||||||
setIsVisible(true);
|
|
||||||
setIsOwned(undefined);
|
|
||||||
setIsEditor(undefined);
|
|
||||||
setFilterUser(undefined);
|
|
||||||
options.setLocation('');
|
|
||||||
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setFilterUser, options]);
|
|
||||||
|
|
||||||
const handleRenameLocation = useCallback(
|
const handleRenameLocation = useCallback(
|
||||||
(newLocation: string) => {
|
(newLocation: string) => {
|
||||||
const data: IRenameLocationData = {
|
const data: IRenameLocationData = {
|
||||||
target: options.location,
|
target: location,
|
||||||
new_location: newLocation
|
new_location: newLocation
|
||||||
};
|
};
|
||||||
library.renameLocation(data, () => {
|
library.renameLocation(data, () => {
|
||||||
options.setLocation(newLocation);
|
setLocation(newLocation);
|
||||||
toast.success(information.locationRenamed);
|
toast.success(information.locationRenamed);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[options, library]
|
[location, setLocation, library]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDownloadCSV = useCallback(() => {
|
const handleDownloadCSV = useCallback(() => {
|
||||||
|
@ -129,7 +64,7 @@ function LibraryPage() {
|
||||||
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
|
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
|
||||||
{showRenameLocation ? (
|
{showRenameLocation ? (
|
||||||
<DlgChangeLocation
|
<DlgChangeLocation
|
||||||
initial={options.location}
|
initial={location}
|
||||||
onChangeLocation={handleRenameLocation}
|
onChangeLocation={handleRenameLocation}
|
||||||
hideWindow={() => setShowRenameLocation(false)}
|
hideWindow={() => setShowRenameLocation(false)}
|
||||||
/>
|
/>
|
||||||
|
@ -145,47 +80,16 @@ function LibraryPage() {
|
||||||
onClick={handleDownloadCSV}
|
onClick={handleDownloadCSV}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<ToolbarSearch
|
<ToolbarSearch total={library.items.length ?? 0} filtered={items.length} />
|
||||||
total={library.items.length ?? 0}
|
|
||||||
filtered={items.length}
|
|
||||||
hasCustomFilter={hasCustomFilter}
|
|
||||||
query={query}
|
|
||||||
onChangeQuery={setQuery}
|
|
||||||
path={path}
|
|
||||||
onChangePath={setPath}
|
|
||||||
head={head}
|
|
||||||
onChangeHead={setHead}
|
|
||||||
isVisible={isVisible}
|
|
||||||
isOwned={isOwned}
|
|
||||||
toggleOwned={() => setIsOwned(prev => toggleTristateFlag(prev))}
|
|
||||||
toggleVisible={() => setIsVisible(prev => toggleTristateFlag(prev))}
|
|
||||||
isEditor={isEditor}
|
|
||||||
toggleEditor={() => setIsEditor(prev => toggleTristateFlag(prev))}
|
|
||||||
filterUser={filterUser}
|
|
||||||
onChangeFilterUser={setFilterUser}
|
|
||||||
resetFilter={resetFilter}
|
|
||||||
folderMode={options.folderMode}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='cc-fade-in flex'>
|
<div className='cc-fade-in flex'>
|
||||||
<ViewSideLocation
|
<ViewSideLocation
|
||||||
isVisible={options.folderMode}
|
isVisible={folderMode}
|
||||||
activeLocation={options.location}
|
|
||||||
onChangeActiveLocation={options.setLocation}
|
|
||||||
subfolders={subfolders}
|
|
||||||
folderTree={library.folders}
|
folderTree={library.folders}
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
toggleSubfolders={() => setSubfolders(prev => !prev)}
|
|
||||||
onRenameLocation={() => setShowRenameLocation(true)}
|
onRenameLocation={() => setShowRenameLocation(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableLibraryItems
|
<TableLibraryItems items={items} />
|
||||||
resetQuery={resetFilter}
|
|
||||||
items={items}
|
|
||||||
folderMode={options.folderMode}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,27 +14,30 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
import { useUsers } from '@/context/UsersContext';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { APP_COLORS } from '@/styling/color';
|
import { APP_COLORS } from '@/styling/color';
|
||||||
import { storage } from '@/utils/constants';
|
|
||||||
|
|
||||||
interface TableLibraryItemsProps {
|
interface TableLibraryItemsProps {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
resetQuery: () => void;
|
|
||||||
folderMode: boolean;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
const columnHelper = createColumnHelper<ILibraryItem>();
|
||||||
|
|
||||||
function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }: TableLibraryItemsProps) {
|
function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getUserLabel } = useUsers();
|
const { getUserLabel } = useUsers();
|
||||||
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
|
|
||||||
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
|
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||||
|
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
||||||
|
|
||||||
|
const itemsPerPage = usePreferencesStore(state => state.libraryPagination);
|
||||||
|
const setItemsPerPage = usePreferencesStore(state => state.setLibraryPagination);
|
||||||
|
|
||||||
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
|
function handleOpenItem(item: ILibraryItem, event: CProps.EventMouse) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
@ -163,7 +166,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
|
||||||
<p>Список схем пуст</p>
|
<p>Список схем пуст</p>
|
||||||
<p className='flex gap-6'>
|
<p className='flex gap-6'>
|
||||||
<TextURL text='Создать схему' href='/library/create' />
|
<TextURL text='Создать схему' href='/library/create' />
|
||||||
<TextURL text='Очистить фильтр' onClick={resetQuery} />
|
<TextURL text='Очистить фильтр' onClick={resetFilter} />
|
||||||
</p>
|
</p>
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import SelectorButton from '@/components/ui/SelectorButton';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
import { useUsers } from '@/context/UsersContext';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { LocationHead } from '@/models/library';
|
import { LocationHead } from '@/models/library';
|
||||||
import { UserID } from '@/models/user';
|
import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
|
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
|
||||||
import { tripleToggleColor } from '@/utils/utils';
|
import { tripleToggleColor } from '@/utils/utils';
|
||||||
|
@ -30,65 +30,38 @@ import { tripleToggleColor } from '@/utils/utils';
|
||||||
interface ToolbarSearchProps {
|
interface ToolbarSearchProps {
|
||||||
total: number;
|
total: number;
|
||||||
filtered: number;
|
filtered: number;
|
||||||
hasCustomFilter: boolean;
|
|
||||||
|
|
||||||
query: string;
|
|
||||||
onChangeQuery: (newValue: string) => void;
|
|
||||||
path: string;
|
|
||||||
onChangePath: (newValue: string) => void;
|
|
||||||
head: LocationHead | undefined;
|
|
||||||
onChangeHead: (newValue: LocationHead | undefined) => void;
|
|
||||||
|
|
||||||
folderMode: boolean;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
|
|
||||||
isVisible: boolean | undefined;
|
|
||||||
toggleVisible: () => void;
|
|
||||||
isOwned: boolean | undefined;
|
|
||||||
toggleOwned: () => void;
|
|
||||||
isEditor: boolean | undefined;
|
|
||||||
toggleEditor: () => void;
|
|
||||||
filterUser: UserID | undefined;
|
|
||||||
onChangeFilterUser: (newValue: UserID | undefined) => void;
|
|
||||||
|
|
||||||
resetFilter: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarSearch({
|
function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
total,
|
|
||||||
filtered,
|
|
||||||
hasCustomFilter,
|
|
||||||
|
|
||||||
query,
|
|
||||||
onChangeQuery,
|
|
||||||
path,
|
|
||||||
onChangePath,
|
|
||||||
head,
|
|
||||||
onChangeHead,
|
|
||||||
|
|
||||||
folderMode,
|
|
||||||
toggleFolderMode,
|
|
||||||
|
|
||||||
isVisible,
|
|
||||||
toggleVisible,
|
|
||||||
isOwned,
|
|
||||||
toggleOwned,
|
|
||||||
isEditor,
|
|
||||||
toggleEditor,
|
|
||||||
filterUser,
|
|
||||||
onChangeFilterUser,
|
|
||||||
|
|
||||||
resetFilter
|
|
||||||
}: ToolbarSearchProps) {
|
|
||||||
const headMenu = useDropdown();
|
const headMenu = useDropdown();
|
||||||
const userMenu = useDropdown();
|
const userMenu = useDropdown();
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
|
|
||||||
|
const query = useLibrarySearchStore(state => state.query);
|
||||||
|
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
||||||
|
const path = useLibrarySearchStore(state => state.path);
|
||||||
|
const setPath = useLibrarySearchStore(state => state.setPath);
|
||||||
|
const head = useLibrarySearchStore(state => state.head);
|
||||||
|
const setHead = useLibrarySearchStore(state => state.setHead);
|
||||||
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
|
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||||
|
const isOwned = useLibrarySearchStore(state => state.isOwned);
|
||||||
|
const toggleOwned = useLibrarySearchStore(state => state.toggleOwned);
|
||||||
|
const isEditor = useLibrarySearchStore(state => state.isEditor);
|
||||||
|
const toggleEditor = useLibrarySearchStore(state => state.toggleEditor);
|
||||||
|
const isVisible = useLibrarySearchStore(state => state.isVisible);
|
||||||
|
const toggleVisible = useLibrarySearchStore(state => state.toggleVisible);
|
||||||
|
const filterUser = useLibrarySearchStore(state => state.filterUser);
|
||||||
|
const setFilterUser = useLibrarySearchStore(state => state.setFilterUser);
|
||||||
|
|
||||||
|
const resetFilter = useLibrarySearchStore(state => state.resetFilter);
|
||||||
|
const hasCustomFilter = useHasCustomFilter();
|
||||||
|
|
||||||
const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined;
|
const userActive = isOwned !== undefined || isEditor !== undefined || filterUser !== undefined;
|
||||||
|
|
||||||
function handleChange(newValue: LocationHead | undefined) {
|
function handleChange(newValue: LocationHead | undefined) {
|
||||||
headMenu.hide();
|
headMenu.hide();
|
||||||
onChangeHead(newValue);
|
setHead(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleFolder() {
|
function handleToggleFolder() {
|
||||||
|
@ -157,7 +130,7 @@ function ToolbarSearch({
|
||||||
className='min-w-[15rem] text-sm mx-1 mb-1'
|
className='min-w-[15rem] text-sm mx-1 mb-1'
|
||||||
items={users}
|
items={users}
|
||||||
value={filterUser}
|
value={filterUser}
|
||||||
onSelectValue={onChangeFilterUser}
|
onSelectValue={setFilterUser}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,7 +150,7 @@ function ToolbarSearch({
|
||||||
noBorder
|
noBorder
|
||||||
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
||||||
query={query}
|
query={query}
|
||||||
onChangeQuery={onChangeQuery}
|
onChangeQuery={setQuery}
|
||||||
/>
|
/>
|
||||||
{!folderMode ? (
|
{!folderMode ? (
|
||||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||||
|
@ -236,7 +209,7 @@ function ToolbarSearch({
|
||||||
noBorder
|
noBorder
|
||||||
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
||||||
query={path}
|
query={path}
|
||||||
onChangeQuery={onChangePath}
|
onChangeQuery={setPath}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,45 +13,36 @@ import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
interface ViewSideLocationProps {
|
interface ViewSideLocationProps {
|
||||||
folderTree: FolderTree;
|
folderTree: FolderTree;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
subfolders: boolean;
|
|
||||||
activeLocation: string;
|
|
||||||
onChangeActiveLocation: (newValue: string) => void;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
toggleSubfolders: () => void;
|
|
||||||
onRenameLocation: () => void;
|
onRenameLocation: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewSideLocation({
|
function ViewSideLocation({ folderTree, isVisible, onRenameLocation }: ViewSideLocationProps) {
|
||||||
folderTree,
|
|
||||||
activeLocation,
|
|
||||||
subfolders,
|
|
||||||
isVisible,
|
|
||||||
onChangeActiveLocation,
|
|
||||||
toggleFolderMode,
|
|
||||||
toggleSubfolders,
|
|
||||||
onRenameLocation
|
|
||||||
}: ViewSideLocationProps) {
|
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { items } = useLibrary();
|
const { items } = useLibrary();
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
|
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||||
|
const subfolders = useLibrarySearchStore(state => state.subfolders);
|
||||||
|
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
||||||
|
|
||||||
const canRename = (() => {
|
const canRename = (() => {
|
||||||
if (activeLocation.length <= 3 || !user) {
|
if (location.length <= 3 || !user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.is_staff) {
|
if (user.is_staff) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const owned = items.filter(item => item.owner == user.id);
|
const owned = items.filter(item => item.owner == user.id);
|
||||||
const located = owned.filter(
|
const located = owned.filter(item => item.location == location || item.location.startsWith(`${location}/`));
|
||||||
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
|
|
||||||
);
|
|
||||||
return located.length !== 0;
|
return located.length !== 0;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -66,7 +57,7 @@ function ViewSideLocation({
|
||||||
.then(() => toast.success(information.pathReady))
|
.then(() => toast.success(information.pathReady))
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
onChangeActiveLocation(target.getPath());
|
setLocation(target.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +88,7 @@ function ViewSideLocation({
|
||||||
onClick={onRenameLocation}
|
onClick={onRenameLocation}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!!activeLocation ? (
|
{!!location ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
||||||
icon={<SubfoldersIcon value={subfolders} />}
|
icon={<SubfoldersIcon value={subfolders} />}
|
||||||
|
@ -112,7 +103,7 @@ function ViewSideLocation({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={activeLocation}
|
value={location}
|
||||||
folderTree={folderTree}
|
folderTree={folderTree}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
onClick={handleClickFolder}
|
onClick={handleClickFolder}
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
import { globals, storage } from '@/utils/constants';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ViewConstituents from '../ViewConstituents';
|
import ViewConstituents from '../ViewConstituents';
|
||||||
|
@ -29,7 +29,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
|
||||||
const [showList, setShowList] = useLocalStorage(storage.rseditShowList, true);
|
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||||
|
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
|
|
||||||
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
|
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
|
||||||
|
@ -78,10 +79,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
activeCst={activeCst}
|
activeCst={activeCst}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
modified={isModified}
|
modified={isModified}
|
||||||
showList={showList}
|
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
onReset={() => setToggleReset(prev => !prev)}
|
onReset={() => setToggleReset(prev => !prev)}
|
||||||
onToggleList={() => setShowList(prev => !prev)}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -29,25 +30,24 @@ interface ToolbarConstituentaProps {
|
||||||
activeCst?: IConstituenta;
|
activeCst?: IConstituenta;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
modified: boolean;
|
modified: boolean;
|
||||||
showList: boolean;
|
|
||||||
|
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onToggleList: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarConstituenta({
|
function ToolbarConstituenta({
|
||||||
activeCst,
|
activeCst,
|
||||||
disabled,
|
disabled,
|
||||||
modified,
|
modified,
|
||||||
showList,
|
|
||||||
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset
|
||||||
onToggleList
|
|
||||||
}: ToolbarConstituentaProps) {
|
}: ToolbarConstituentaProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
|
||||||
|
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||||
|
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
|
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
|
||||||
|
@ -104,7 +104,7 @@ function ToolbarConstituenta({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Отображение списка конституент'
|
title='Отображение списка конституент'
|
||||||
icon={showList ? <IconList size='1.25rem' className='icon-primary' /> : <IconListOff size='1.25rem' />}
|
icon={showList ? <IconList size='1.25rem' className='icon-primary' /> : <IconListOff size='1.25rem' />}
|
||||||
onClick={onToggleList}
|
onClick={toggleList}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{controller.isContentEditable ? (
|
{controller.isContentEditable ? (
|
||||||
|
|
|
@ -13,14 +13,13 @@ import Overlay from '@/components/ui/Overlay';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import DlgShowAST from '@/dialogs/DlgShowAST';
|
import DlgShowAST from '@/dialogs/DlgShowAST';
|
||||||
import useCheckConstituenta from '@/hooks/useCheckConstituenta';
|
import useCheckConstituenta from '@/hooks/useCheckConstituenta';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||||
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
||||||
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
||||||
import { TokenID } from '@/models/rslang';
|
import { TokenID } from '@/models/rslang';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { transformAST } from '@/utils/codemirror';
|
import { transformAST } from '@/utils/codemirror';
|
||||||
import { storage } from '@/utils/constants';
|
|
||||||
import { errors, labelTypification } from '@/utils/labels';
|
import { errors, labelTypification } from '@/utils/labels';
|
||||||
|
|
||||||
import ParsingResult from './ParsingResult';
|
import ParsingResult from './ParsingResult';
|
||||||
|
@ -64,10 +63,10 @@ function EditorRSExpression({
|
||||||
const { resetParse } = parser;
|
const { resetParse } = parser;
|
||||||
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
|
||||||
|
const showControls = usePreferencesStore(state => state.showExpressionControls);
|
||||||
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
||||||
const [expression, setExpression] = useState('');
|
const [expression, setExpression] = useState('');
|
||||||
const [showAST, setShowAST] = useState(false);
|
const [showAST, setShowAST] = useState(false);
|
||||||
const [showControls, setShowControls] = useLocalStorage(storage.rseditShowControls, true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
|
@ -154,13 +153,7 @@ function EditorRSExpression({
|
||||||
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
|
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ToolbarRSExpression
|
<ToolbarRSExpression disabled={disabled} showAST={handleShowAST} showTypeGraph={onShowTypeGraph} />
|
||||||
disabled={disabled}
|
|
||||||
showControls={showControls}
|
|
||||||
showAST={handleShowAST}
|
|
||||||
toggleControls={() => setShowControls(prev => !prev)}
|
|
||||||
showTypeGraph={onShowTypeGraph}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Overlay
|
<Overlay
|
||||||
position='top-[-0.5rem] right-1/2 translate-x-1/2'
|
position='top-[-0.5rem] right-1/2 translate-x-1/2'
|
||||||
|
|
|
@ -3,24 +3,18 @@ import { CProps } from '@/components/props';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
interface ToolbarRSExpressionProps {
|
interface ToolbarRSExpressionProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
showControls: boolean;
|
|
||||||
|
|
||||||
toggleControls: () => void;
|
|
||||||
showAST: (event: CProps.EventMouse) => void;
|
showAST: (event: CProps.EventMouse) => void;
|
||||||
showTypeGraph: (event: CProps.EventMouse) => void;
|
showTypeGraph: (event: CProps.EventMouse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarRSExpression({
|
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
|
||||||
disabled,
|
|
||||||
showControls,
|
|
||||||
showTypeGraph,
|
|
||||||
toggleControls,
|
|
||||||
showAST
|
|
||||||
}: ToolbarRSExpressionProps) {
|
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
const showControls = usePreferencesStore(state => state.showExpressionControls);
|
||||||
|
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-[-0.5rem] right-0' layer='z-pop' className='cc-icons'>
|
<Overlay position='top-[-0.5rem] right-0' layer='z-pop' className='cc-icons'>
|
||||||
|
|
|
@ -18,12 +18,12 @@ import Overlay from '@/components/ui/Overlay';
|
||||||
import Tooltip from '@/components/ui/Tooltip';
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
import ValueIcon from '@/components/ui/ValueIcon';
|
import ValueIcon from '@/components/ui/ValueIcon';
|
||||||
import { useAccessMode } from '@/context/AccessModeContext';
|
import { useAccessMode } from '@/context/AccessModeContext';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
import { useUsers } from '@/context/UsersContext';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
||||||
import { UserID, UserLevel } from '@/models/user';
|
import { UserID, UserLevel } from '@/models/user';
|
||||||
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { prompts } from '@/utils/labels';
|
import { prompts } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
const { accessLevel } = useAccessMode();
|
const { accessLevel } = useAccessMode();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { setLocation, setFolderMode } = useConceptOptions();
|
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
|
||||||
const ownerSelector = useDropdown();
|
const ownerSelector = useDropdown();
|
||||||
const onSelectUser = useCallback(
|
const onSelectUser = useCallback(
|
||||||
|
@ -61,10 +61,9 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLocation(item.location);
|
setLocation(item.location);
|
||||||
setFolderMode(true);
|
|
||||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
router.push(urls.library, event.ctrlKey || event.metaKey);
|
||||||
},
|
},
|
||||||
[setLocation, setFolderMode, item, router]
|
[setLocation, item, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
|
|
@ -25,13 +25,12 @@ import { CProps } from '@/components/props';
|
||||||
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
import DlgGraphParams from '@/dialogs/DlgGraphParams';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
|
|
||||||
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
||||||
import { isBasicConcept } from '@/models/rsformAPI';
|
import { isBasicConcept } from '@/models/rsformAPI';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
|
import { useTermGraphStore } from '@/stores/termGraph';
|
||||||
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
||||||
import { PARAMETER, storage } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errors } from '@/utils/labels';
|
import { errors } from '@/utils/labels';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
@ -62,29 +61,14 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
const { addSelectedNodes } = store.getState();
|
const { addSelectedNodes } = store.getState();
|
||||||
|
|
||||||
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
const [showParamsDialog, setShowParamsDialog] = useState(false);
|
||||||
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
|
|
||||||
noHermits: true,
|
|
||||||
noTemplates: false,
|
|
||||||
noTransitive: true,
|
|
||||||
noText: false,
|
|
||||||
foldDerived: false,
|
|
||||||
|
|
||||||
focusShowInputs: true,
|
const filter = useTermGraphStore(state => state.filter);
|
||||||
focusShowOutputs: true,
|
const setFilter = useTermGraphStore(state => state.setFilter);
|
||||||
|
const coloring = useTermGraphStore(state => state.coloring);
|
||||||
allowBase: true,
|
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||||
allowStruct: true,
|
|
||||||
allowTerm: true,
|
|
||||||
allowAxiom: true,
|
|
||||||
allowFunction: true,
|
|
||||||
allowPredicate: true,
|
|
||||||
allowConstant: true,
|
|
||||||
allowTheorem: true
|
|
||||||
});
|
|
||||||
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
|
|
||||||
|
|
||||||
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
const filteredGraph = useGraphFilter(controller.schema, filterParams, focusCst);
|
const filteredGraph = useGraphFilter(controller.schema, filter, focusCst);
|
||||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||||
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
@ -139,7 +123,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
data: {
|
data: {
|
||||||
fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring),
|
fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring),
|
||||||
label: cst.alias,
|
label: cst.alias,
|
||||||
description: !filterParams.noText ? cst.term_resolved : ''
|
description: !filter.noText ? cst.term_resolved : ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -167,24 +151,15 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
applyLayout(newNodes, newEdges, !filterParams.noText);
|
applyLayout(newNodes, newEdges, !filter.noText);
|
||||||
|
|
||||||
setNodes(newNodes);
|
setNodes(newNodes);
|
||||||
setEdges(newEdges);
|
setEdges(newEdges);
|
||||||
}, [
|
}, [controller.schema, filteredGraph, setNodes, setEdges, filter.noText, controller.selected, focusCst, coloring]);
|
||||||
controller.schema,
|
|
||||||
filteredGraph,
|
|
||||||
setNodes,
|
|
||||||
setEdges,
|
|
||||||
filterParams.noText,
|
|
||||||
controller.selected,
|
|
||||||
focusCst,
|
|
||||||
coloring
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNeedReset(true);
|
setNeedReset(true);
|
||||||
}, [controller.schema, filterParams.noText, focusCst, coloring, flow.viewportInitialized]);
|
}, [controller.schema, filter.noText, focusCst, coloring, flow.viewportInitialized]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!controller.schema || !needReset || !flow.viewportInitialized) {
|
if (!controller.schema || !needReset || !flow.viewportInitialized) {
|
||||||
|
@ -198,7 +173,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||||
}, PARAMETER.minimalTimeout);
|
}, PARAMETER.minimalTimeout);
|
||||||
}, [toggleResetView, flow, focusCst, filterParams]);
|
}, [toggleResetView, flow, focusCst, filter]);
|
||||||
|
|
||||||
function handleSetSelected(newSelection: number[]) {
|
function handleSetSelected(newSelection: number[]) {
|
||||||
controller.setSelected(newSelection);
|
controller.setSelected(newSelection);
|
||||||
|
@ -220,10 +195,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
controller.promptDeleteCst();
|
controller.promptDeleteCst();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeParams(params: GraphFilterParams) {
|
|
||||||
setFilterParams(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSaveImage() {
|
function handleSaveImage() {
|
||||||
if (!controller.schema) {
|
if (!controller.schema) {
|
||||||
return;
|
return;
|
||||||
|
@ -283,10 +254,10 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFoldDerived() {
|
function handleFoldDerived() {
|
||||||
setFilterParams(prev => ({
|
setFilter({
|
||||||
...prev,
|
...filter,
|
||||||
foldDerived: !prev.foldDerived
|
foldDerived: !filter.foldDerived
|
||||||
}));
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setToggleResetView(prev => !prev);
|
setToggleResetView(prev => !prev);
|
||||||
}, PARAMETER.graphRefreshDelay);
|
}, PARAMETER.graphRefreshDelay);
|
||||||
|
@ -325,17 +296,13 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showParamsDialog ? (
|
{showParamsDialog ? (
|
||||||
<DlgGraphParams
|
<DlgGraphParams hideWindow={() => setShowParamsDialog(false)} initial={filter} onConfirm={setFilter} />
|
||||||
hideWindow={() => setShowParamsDialog(false)}
|
|
||||||
initial={filterParams}
|
|
||||||
onConfirm={handleChangeParams}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
||||||
<ToolbarTermGraph
|
<ToolbarTermGraph
|
||||||
noText={filterParams.noText}
|
noText={filter.noText}
|
||||||
foldDerived={filterParams.foldDerived}
|
foldDerived={filter.foldDerived}
|
||||||
showParamsDialog={() => setShowParamsDialog(true)}
|
showParamsDialog={() => setShowParamsDialog(true)}
|
||||||
onCreate={handleCreateCst}
|
onCreate={handleCreateCst}
|
||||||
onDelete={handleDeleteCst}
|
onDelete={handleDeleteCst}
|
||||||
|
@ -343,10 +310,10 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
onSaveImage={handleSaveImage}
|
onSaveImage={handleSaveImage}
|
||||||
toggleFoldDerived={handleFoldDerived}
|
toggleFoldDerived={handleFoldDerived}
|
||||||
toggleNoText={() =>
|
toggleNoText={() =>
|
||||||
setFilterParams(prev => ({
|
setFilter({
|
||||||
...prev,
|
...filter,
|
||||||
noText: !prev.noText
|
noText: !filter.noText
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{!focusCst ? (
|
{!focusCst ? (
|
||||||
|
@ -367,19 +334,19 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
<ToolbarFocusedCst
|
<ToolbarFocusedCst
|
||||||
center={focusCst}
|
center={focusCst}
|
||||||
reset={() => handleSetFocus(undefined)}
|
reset={() => handleSetFocus(undefined)}
|
||||||
showInputs={filterParams.focusShowInputs}
|
showInputs={filter.focusShowInputs}
|
||||||
showOutputs={filterParams.focusShowOutputs}
|
showOutputs={filter.focusShowOutputs}
|
||||||
toggleShowInputs={() =>
|
toggleShowInputs={() =>
|
||||||
setFilterParams(prev => ({
|
setFilter({
|
||||||
...prev,
|
...filter,
|
||||||
focusShowInputs: !prev.focusShowInputs
|
focusShowInputs: !filter.focusShowInputs
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
toggleShowOutputs={() =>
|
toggleShowOutputs={() =>
|
||||||
setFilterParams(prev => ({
|
setFilter({
|
||||||
...prev,
|
...filter,
|
||||||
focusShowOutputs: !prev.focusShowOutputs
|
focusShowOutputs: !filter.focusShowOutputs
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -7,13 +7,13 @@ import { CProps } from '@/components/props';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { GraphColoring } from '@/models/miscellaneous';
|
import { GraphColoring } from '@/models/miscellaneous';
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
|
import { useTermGraphStore } from '@/stores/termGraph';
|
||||||
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
||||||
import { globals, PARAMETER, prefixes, storage } from '@/utils/constants';
|
import { globals, PARAMETER, prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
interface ViewHiddenProps {
|
interface ViewHiddenProps {
|
||||||
items: ConstituentaID[];
|
items: ConstituentaID[];
|
||||||
|
@ -29,7 +29,9 @@ interface ViewHiddenProps {
|
||||||
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
|
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const localSelected = items.filter(id => selected.includes(id));
|
const localSelected = items.filter(id => selected.includes(id));
|
||||||
const [isFolded, setIsFolded] = useLocalStorage(storage.rsgraphFoldHidden, false);
|
|
||||||
|
const isFolded = useTermGraphStore(state => state.foldHidden);
|
||||||
|
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
||||||
const { setHoverCst } = useConceptOptions();
|
const { setHoverCst } = useConceptOptions();
|
||||||
const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
|
const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
noHover
|
noHover
|
||||||
title={!isFolded ? 'Свернуть' : 'Развернуть'}
|
title={!isFolded ? 'Свернуть' : 'Развернуть'}
|
||||||
icon={!isFolded ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
|
icon={!isFolded ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
|
||||||
onClick={() => setIsFolded(prev => !prev)}
|
onClick={toggleFolded}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<div className={clsx('pt-2 clr-input border-x pb-2', { 'border-b rounded-b-md': isFolded })}>
|
<div className={clsx('pt-2 clr-input border-x pb-2', { 'border-b rounded-b-md': isFolded })}>
|
||||||
|
@ -87,7 +89,6 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
>
|
>
|
||||||
{items.map(cstID => {
|
{items.map(cstID => {
|
||||||
const cst = schema.cstByID.get(cstID)!;
|
const cst = schema.cstByID.get(cstID)!;
|
||||||
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
|
|
||||||
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -95,7 +96,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
type='button'
|
type='button'
|
||||||
className='min-w-[3rem] rounded-md text-center select-none'
|
className='min-w-[3rem] rounded-md text-center select-none'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colorBgGraphNode(cst, adjustedColoring),
|
backgroundColor: colorBgGraphNode(cst, coloringScheme),
|
||||||
...(localSelected.includes(cstID)
|
...(localSelected.includes(cstID)
|
||||||
? {
|
? {
|
||||||
outlineWidth: '2px',
|
outlineWidth: '2px',
|
||||||
|
|
152
rsconcept/frontend/src/stores/librarySearch.ts
Normal file
152
rsconcept/frontend/src/stores/librarySearch.ts
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { LocationHead } from '@/models/library';
|
||||||
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
|
import { UserID } from '@/models/user';
|
||||||
|
import { toggleTristateFlag } from '@/utils/utils';
|
||||||
|
|
||||||
|
interface LibrarySearchStore {
|
||||||
|
folderMode: boolean;
|
||||||
|
toggleFolderMode: () => void;
|
||||||
|
|
||||||
|
subfolders: boolean;
|
||||||
|
toggleSubfolders: () => void;
|
||||||
|
|
||||||
|
query: string;
|
||||||
|
setQuery: (value: string) => void;
|
||||||
|
|
||||||
|
path: string;
|
||||||
|
setPath: (value: string) => void;
|
||||||
|
|
||||||
|
location: string;
|
||||||
|
setLocation: (value: string) => void;
|
||||||
|
|
||||||
|
head: LocationHead | undefined;
|
||||||
|
setHead: (value: LocationHead | undefined) => void;
|
||||||
|
|
||||||
|
isVisible: boolean | undefined;
|
||||||
|
toggleVisible: () => void;
|
||||||
|
|
||||||
|
isOwned: boolean | undefined;
|
||||||
|
toggleOwned: () => void;
|
||||||
|
|
||||||
|
isEditor: boolean | undefined;
|
||||||
|
toggleEditor: () => void;
|
||||||
|
|
||||||
|
filterUser: UserID | undefined;
|
||||||
|
setFilterUser: (value: UserID | undefined) => void;
|
||||||
|
|
||||||
|
resetFilter: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLibrarySearchStore = create<LibrarySearchStore>()(
|
||||||
|
persist(
|
||||||
|
set => ({
|
||||||
|
folderMode: true,
|
||||||
|
toggleFolderMode: () => set(state => ({ folderMode: !state.folderMode })),
|
||||||
|
|
||||||
|
subfolders: false,
|
||||||
|
toggleSubfolders: () => set(state => ({ subfolders: !state.subfolders })),
|
||||||
|
|
||||||
|
query: '',
|
||||||
|
setQuery: value => set({ query: value }),
|
||||||
|
|
||||||
|
path: '',
|
||||||
|
setPath: value => set({ path: value }),
|
||||||
|
|
||||||
|
location: '',
|
||||||
|
setLocation: value => set(!!value ? { location: value, folderMode: true } : { location: '' }),
|
||||||
|
|
||||||
|
head: undefined,
|
||||||
|
setHead: value => set({ head: value }),
|
||||||
|
|
||||||
|
isVisible: true,
|
||||||
|
toggleVisible: () => set(state => ({ isVisible: toggleTristateFlag(state.isVisible) })),
|
||||||
|
|
||||||
|
isOwned: undefined,
|
||||||
|
toggleOwned: () => set(state => ({ isOwned: toggleTristateFlag(state.isOwned) })),
|
||||||
|
|
||||||
|
isEditor: undefined,
|
||||||
|
toggleEditor: () => set(state => ({ isEditor: toggleTristateFlag(state.isEditor) })),
|
||||||
|
|
||||||
|
filterUser: undefined,
|
||||||
|
setFilterUser: value => set({ filterUser: value }),
|
||||||
|
|
||||||
|
resetFilter: () =>
|
||||||
|
set(() => ({
|
||||||
|
query: '',
|
||||||
|
path: '',
|
||||||
|
location: '',
|
||||||
|
head: undefined,
|
||||||
|
isVisible: true,
|
||||||
|
isOwned: undefined,
|
||||||
|
isEditor: undefined,
|
||||||
|
filterUser: undefined
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
partialize: state => ({
|
||||||
|
folderMode: state.folderMode,
|
||||||
|
subfolders: state.subfolders,
|
||||||
|
|
||||||
|
location: state.location,
|
||||||
|
head: state.head,
|
||||||
|
isVisible: state.isVisible,
|
||||||
|
isOwned: state.isOwned,
|
||||||
|
isEditor: state.isEditor,
|
||||||
|
filterUser: state.filterUser
|
||||||
|
}),
|
||||||
|
name: 'portal.library.search'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Utility function that indicates if custom filter is set. */
|
||||||
|
export function useHasCustomFilter(): boolean {
|
||||||
|
const path = useLibrarySearchStore(state => state.path);
|
||||||
|
const query = useLibrarySearchStore(state => state.query);
|
||||||
|
const head = useLibrarySearchStore(state => state.head);
|
||||||
|
const isEditor = useLibrarySearchStore(state => state.isEditor);
|
||||||
|
const isOwned = useLibrarySearchStore(state => state.isOwned);
|
||||||
|
const isVisible = useLibrarySearchStore(state => state.isVisible);
|
||||||
|
const filterUser = useLibrarySearchStore(state => state.filterUser);
|
||||||
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
|
return (
|
||||||
|
!!path ||
|
||||||
|
!!query ||
|
||||||
|
!!location ||
|
||||||
|
head !== undefined ||
|
||||||
|
isEditor !== undefined ||
|
||||||
|
isOwned !== undefined ||
|
||||||
|
isVisible !== true ||
|
||||||
|
filterUser !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility function that returns the current library filter. */
|
||||||
|
export function useLibraryFilter(): ILibraryFilter {
|
||||||
|
const head = useLibrarySearchStore(state => state.head);
|
||||||
|
const path = useLibrarySearchStore(state => state.path);
|
||||||
|
const query = useLibrarySearchStore(state => state.query);
|
||||||
|
const isEditor = useLibrarySearchStore(state => state.isEditor);
|
||||||
|
const isOwned = useLibrarySearchStore(state => state.isOwned);
|
||||||
|
const isVisible = useLibrarySearchStore(state => state.isVisible);
|
||||||
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
|
const subfolders = useLibrarySearchStore(state => state.subfolders);
|
||||||
|
const location = useLibrarySearchStore(state => state.location);
|
||||||
|
const filterUser = useLibrarySearchStore(state => state.filterUser);
|
||||||
|
return {
|
||||||
|
head: head,
|
||||||
|
path: path,
|
||||||
|
query: query,
|
||||||
|
isEditor: isEditor,
|
||||||
|
isOwned: isOwned,
|
||||||
|
isVisible: isVisible,
|
||||||
|
folderMode: folderMode,
|
||||||
|
subfolders: subfolders,
|
||||||
|
location: location,
|
||||||
|
filterUser: filterUser
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,20 +1,70 @@
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
interface PreferencesStore {
|
interface PreferencesStore {
|
||||||
|
darkMode: boolean;
|
||||||
|
toggleDarkMode: () => void;
|
||||||
|
|
||||||
showHelp: boolean;
|
showHelp: boolean;
|
||||||
adminMode: boolean;
|
|
||||||
toggleShowHelp: () => void;
|
toggleShowHelp: () => void;
|
||||||
|
|
||||||
|
adminMode: boolean;
|
||||||
toggleAdminMode: () => void;
|
toggleAdminMode: () => void;
|
||||||
|
|
||||||
|
libraryPagination: number;
|
||||||
|
setLibraryPagination: (value: number) => void;
|
||||||
|
|
||||||
|
showCstSideList: boolean;
|
||||||
|
toggleShowCstSideList: () => void;
|
||||||
|
|
||||||
|
showExpressionControls: boolean;
|
||||||
|
toggleShowExpressionControls: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePreferencesStore = create<PreferencesStore>()(
|
export const usePreferencesStore = create<PreferencesStore>()(
|
||||||
persist(
|
persist(
|
||||||
set => ({
|
set => ({
|
||||||
|
darkMode: initializeDarkMode(),
|
||||||
|
toggleDarkMode: () => {
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
set(state => applyDarkMode(!state.darkMode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.innerHTML = `
|
||||||
|
* {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
flushSync(() => {
|
||||||
|
set(state => applyDarkMode(!state.darkMode));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => document.head.removeChild(style), PARAMETER.moveDuration);
|
||||||
|
},
|
||||||
|
|
||||||
showHelp: true,
|
showHelp: true,
|
||||||
adminMode: false,
|
|
||||||
toggleShowHelp: () => set(state => ({ showHelp: !state.showHelp })),
|
toggleShowHelp: () => set(state => ({ showHelp: !state.showHelp })),
|
||||||
toggleAdminMode: () => set(state => ({ adminMode: !state.adminMode }))
|
|
||||||
|
adminMode: false,
|
||||||
|
toggleAdminMode: () => set(state => ({ adminMode: !state.adminMode })),
|
||||||
|
|
||||||
|
libraryPagination: 50,
|
||||||
|
setLibraryPagination: value => set({ libraryPagination: value }),
|
||||||
|
|
||||||
|
showCstSideList: true,
|
||||||
|
toggleShowCstSideList: () => set(state => ({ showCstSideList: !state.showCstSideList })),
|
||||||
|
|
||||||
|
showExpressionControls: true,
|
||||||
|
toggleShowExpressionControls: () => set(state => ({ showExpressionControls: !state.showExpressionControls }))
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@ -22,3 +72,31 @@ export const usePreferencesStore = create<PreferencesStore>()(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function initializeDarkMode(): boolean {
|
||||||
|
let isDark = false;
|
||||||
|
if ('portal.preferences' in localStorage) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
const preferences = JSON.parse(localStorage.getItem('portal.preferences') ?? '{}').state as PreferencesStore;
|
||||||
|
isDark = preferences.darkMode;
|
||||||
|
} else if (window.matchMedia) {
|
||||||
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
return isDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDarkMode(isDark: boolean) {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
if (isDark) {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
root.setAttribute('data-color-scheme', !isDark ? 'light' : 'dark');
|
||||||
|
return { darkMode: isDark };
|
||||||
|
}
|
||||||
|
|
52
rsconcept/frontend/src/stores/termGraph.ts
Normal file
52
rsconcept/frontend/src/stores/termGraph.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
|
||||||
|
|
||||||
|
interface TermGraphStore {
|
||||||
|
filter: GraphFilterParams;
|
||||||
|
setFilter: (value: GraphFilterParams) => void;
|
||||||
|
|
||||||
|
foldHidden: boolean;
|
||||||
|
toggleFoldHidden: () => void;
|
||||||
|
|
||||||
|
coloring: GraphColoring;
|
||||||
|
setColoring: (value: GraphColoring) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTermGraphStore = create<TermGraphStore>()(
|
||||||
|
persist(
|
||||||
|
set => ({
|
||||||
|
filter: {
|
||||||
|
noTemplates: false,
|
||||||
|
noHermits: true,
|
||||||
|
noTransitive: true,
|
||||||
|
noText: false,
|
||||||
|
foldDerived: false,
|
||||||
|
|
||||||
|
focusShowInputs: true,
|
||||||
|
focusShowOutputs: true,
|
||||||
|
|
||||||
|
allowBase: true,
|
||||||
|
allowStruct: true,
|
||||||
|
allowTerm: true,
|
||||||
|
allowAxiom: true,
|
||||||
|
allowFunction: true,
|
||||||
|
allowPredicate: true,
|
||||||
|
allowConstant: true,
|
||||||
|
allowTheorem: true
|
||||||
|
},
|
||||||
|
setFilter: value => set({ filter: value }),
|
||||||
|
|
||||||
|
foldHidden: false,
|
||||||
|
toggleFoldHidden: () => set(state => ({ foldHidden: !state.foldHidden })),
|
||||||
|
|
||||||
|
coloring: 'type',
|
||||||
|
setColoring: value => set({ coloring: value })
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
name: 'portal.termGraph'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
|
@ -108,25 +108,6 @@ export const external_urls = {
|
||||||
export const storage = {
|
export const storage = {
|
||||||
PREFIX: 'portal.',
|
PREFIX: 'portal.',
|
||||||
|
|
||||||
themeDark: 'theme.dark',
|
|
||||||
|
|
||||||
rseditShowList: 'rsedit.show_list',
|
|
||||||
rseditShowControls: 'rsedit.show_controls',
|
|
||||||
|
|
||||||
librarySearchHead: 'library.search.head',
|
|
||||||
librarySearchFolderMode: 'library.search.folder_mode',
|
|
||||||
librarySearchSubfolders: 'library.search.subfolders',
|
|
||||||
librarySearchLocation: 'library.search.location',
|
|
||||||
librarySearchVisible: 'library.search.visible',
|
|
||||||
librarySearchOwned: 'library.search.owned',
|
|
||||||
librarySearchEditor: 'library.search.editor',
|
|
||||||
librarySearchUser: 'library.search.user',
|
|
||||||
libraryPagination: 'library.pagination',
|
|
||||||
|
|
||||||
rsgraphFilter: 'rsgraph.filter2',
|
|
||||||
rsgraphColoring: 'rsgraph.coloring',
|
|
||||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
|
||||||
|
|
||||||
ossShowGrid: 'oss.show_grid',
|
ossShowGrid: 'oss.show_grid',
|
||||||
ossEdgeStraight: 'oss.edge_straight',
|
ossEdgeStraight: 'oss.edge_straight',
|
||||||
ossEdgeAnimate: 'oss.edge_animate',
|
ossEdgeAnimate: 'oss.edge_animate',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user