R: Migrating to zustand for local state management pt2

This commit is contained in:
Ivan 2025-01-15 16:06:42 +03:00
parent 4c9b0b28b8
commit 8c3e5dfd4d
26 changed files with 438 additions and 422 deletions

View File

@ -119,6 +119,7 @@
"NUMR",
"Opencorpora",
"overscroll",
"partialize",
"passwordreset",
"perfectivity",
"PNCT",

View File

@ -20,22 +20,6 @@
/>
<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>
<body>
<div id="root"></div>

View File

@ -1,11 +1,11 @@
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { usePreferencesStore } from '@/stores/preferences';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
function ToasterThemed(props: ToasterThemedProps) {
const { darkMode } = useConceptOptions();
const darkMode = usePreferencesStore(state => state.darkMode);
return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
}

View File

@ -1,8 +1,8 @@
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useWindowSize from '@/hooks/useWindowSize';
import { usePreferencesStore } from '@/stores/preferences';
function Logo() {
const { darkMode } = useConceptOptions();
const darkMode = usePreferencesStore(state => state.darkMode);
const size = useWindowSize();
return (

View File

@ -1,12 +1,13 @@
import clsx from 'clsx';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useAppLayoutStore } from '@/stores/appLayout';
import { usePreferencesStore } from '@/stores/preferences';
import { globals, PARAMETER } from '@/utils/constants';
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 noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const toggleNoNavigation = useAppLayoutStore(state => state.toggleNoNavigation);

View File

@ -16,7 +16,6 @@ import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences';
@ -28,10 +27,11 @@ interface UserDropdownProps {
}
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const { darkMode, toggleDarkMode } = useConceptOptions();
const router = useConceptNavigation();
const { user, logout } = useAuth();
const darkMode = usePreferencesStore(state => state.darkMode);
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
const showHelp = usePreferencesStore(state => state.showHelp);
const toggleShowHelp = usePreferencesStore(state => state.toggleShowHelp);
const adminMode = usePreferencesStore(state => state.adminMode);

View File

@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
import { forwardRef, useRef } from 'react';
import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
import { extractGlobals } from '@/models/rslangAPI';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/color';
import { ccBracketMatching } from './bracketMatching';
@ -64,7 +64,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
},
ref
) => {
const { darkMode } = useConceptOptions();
const darkMode = usePreferencesStore(state => state.darkMode);
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;

View File

@ -9,10 +9,10 @@ import { EditorView } from 'codemirror';
import { forwardRef, useRef, useState } from 'react';
import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import DlgEditReference from '@/dialogs/DlgEditReference';
import { ReferenceType } from '@/models/language';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/color';
import { CodeMirrorWrapper } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
@ -92,7 +92,7 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
},
ref
) => {
const { darkMode } = useConceptOptions();
const darkMode = usePreferencesStore(state => state.darkMode);
const [isFocused, setIsFocused] = useState(false);

View File

@ -5,7 +5,7 @@ import { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { usePreferencesStore } from '@/stores/preferences';
export type { PlacesType } from 'react-tooltip';
@ -29,7 +29,7 @@ function Tooltip({
style,
...restProps
}: TooltipProps) {
const { darkMode } = useConceptOptions();
const darkMode = usePreferencesStore(state => state.darkMode);
if (typeof window === 'undefined') {
return null;
}

View File

@ -1,26 +1,15 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { flushSync } from 'react-dom';
import { createContext, useContext, useState } from 'react';
import InfoConstituenta from '@/components/info/InfoConstituenta';
import Loader from '@/components/ui/Loader';
import Tooltip from '@/components/ui/Tooltip';
import useLocalStorage from '@/hooks/useLocalStorage';
import { IConstituenta } from '@/models/rsform';
import { globals, PARAMETER, storage } from '@/utils/constants';
import { globals } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels';
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;
}
@ -34,59 +23,11 @@ export const useConceptOptions = () => {
};
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);
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 (
<OptionsContext
value={{
darkMode,
folderMode,
setFolderMode,
location,
setLocation,
toggleDarkMode: toggleDarkMode,
setHoverCst
}}
>

View File

@ -20,21 +20,23 @@ import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information } from '@/utils/labels';
function FormCreateItem() {
const router = useConceptNavigation();
const options = useConceptOptions();
const { user } = useAuth();
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 [title, setTitle] = useState('');
const [alias, setAlias] = useState('');
@ -81,7 +83,7 @@ function FormCreateItem() {
file: file,
fileName: file?.name
};
options.setLocation(location);
setSearchLocation(location);
createItem(data, newItem => {
toast.success(information.newLibraryItem);
if (itemType == LibraryItemType.RSFORM) {
@ -108,11 +110,11 @@ function FormCreateItem() {
}, []);
useEffect(() => {
if (!options.location) {
if (!searchLocation) {
return;
}
handleSelectLocation(options.location);
}, [options.location, handleSelectLocation]);
handleSelectLocation(searchLocation);
}, [searchLocation, handleSelectLocation]);
useEffect(() => {
if (itemType !== LibraryItemType.RSFORM) {

View File

@ -1,25 +1,20 @@
'use client';
import fileDownload from 'js-file-download';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { toast } from 'react-toastify';
import { IconCSV } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import useLocalStorage from '@/hooks/useLocalStorage';
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
import { ILibraryFilter } from '@/models/miscellaneous';
import { UserID } from '@/models/user';
import { IRenameLocationData } from '@/models/library';
import { useAppLayoutStore } from '@/stores/appLayout';
import { storage } from '@/utils/constants';
import { useLibraryFilter, useLibrarySearchStore } from '@/stores/librarySearch';
import { information } from '@/utils/labels';
import { convertToCSV, toggleTristateFlag } from '@/utils/utils';
import { convertToCSV } from '@/utils/utils';
import TableLibraryItems from './TableLibraryItems';
import ToolbarSearch from './ToolbarSearch';
@ -27,89 +22,29 @@ import ViewSideLocation from './ViewSideLocation';
function LibraryPage() {
const library = useLibrary();
const { user } = useAuth();
const [items, setItems] = useState<ILibraryItem[]>([]);
const options = useConceptOptions();
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const [query, setQuery] = useState('');
const [path, setPath] = useState('');
const folderMode = useLibrarySearchStore(state => state.folderMode);
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 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(
(newLocation: string) => {
const data: IRenameLocationData = {
target: options.location,
target: location,
new_location: newLocation
};
library.renameLocation(data, () => {
options.setLocation(newLocation);
setLocation(newLocation);
toast.success(information.locationRenamed);
});
},
[options, library]
[location, setLocation, library]
);
const handleDownloadCSV = useCallback(() => {
@ -129,7 +64,7 @@ function LibraryPage() {
<DataLoader isLoading={library.loading} error={library.loadingError} hasNoData={library.items.length === 0}>
{showRenameLocation ? (
<DlgChangeLocation
initial={options.location}
initial={location}
onChangeLocation={handleRenameLocation}
hideWindow={() => setShowRenameLocation(false)}
/>
@ -145,47 +80,16 @@ function LibraryPage() {
onClick={handleDownloadCSV}
/>
</Overlay>
<ToolbarSearch
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}
/>
<ToolbarSearch total={library.items.length ?? 0} filtered={items.length} />
<div className='cc-fade-in flex'>
<ViewSideLocation
isVisible={options.folderMode}
activeLocation={options.location}
onChangeActiveLocation={options.setLocation}
subfolders={subfolders}
isVisible={folderMode}
folderTree={library.folders}
toggleFolderMode={toggleFolderMode}
toggleSubfolders={() => setSubfolders(prev => !prev)}
onRenameLocation={() => setShowRenameLocation(true)}
/>
<TableLibraryItems
resetQuery={resetFilter}
items={items}
folderMode={options.folderMode}
toggleFolderMode={toggleFolderMode}
/>
<TableLibraryItems items={items} />
</div>
</DataLoader>
);

View File

@ -14,27 +14,30 @@ import MiniButton from '@/components/ui/MiniButton';
import TextURL from '@/components/ui/TextURL';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useUsers } from '@/context/UsersContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { ILibraryItem, LibraryItemType } from '@/models/library';
import { useFitHeight } from '@/stores/appLayout';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/color';
import { storage } from '@/utils/constants';
interface TableLibraryItemsProps {
items: ILibraryItem[];
resetQuery: () => void;
folderMode: boolean;
toggleFolderMode: () => void;
}
const columnHelper = createColumnHelper<ILibraryItem>();
function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }: TableLibraryItemsProps) {
function TableLibraryItems({ items }: TableLibraryItemsProps) {
const router = useConceptNavigation();
const intl = useIntl();
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) {
const selection = window.getSelection();
@ -163,7 +166,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
<p>Список схем пуст</p>
<p className='flex gap-6'>
<TextURL text='Создать схему' href='/library/create' />
<TextURL text='Очистить фильтр' onClick={resetQuery} />
<TextURL text='Очистить фильтр' onClick={resetFilter} />
</p>
</FlexColumn>
}

View File

@ -22,7 +22,7 @@ import SelectorButton from '@/components/ui/SelectorButton';
import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown';
import { LocationHead } from '@/models/library';
import { UserID } from '@/models/user';
import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
import { prefixes } from '@/utils/constants';
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
import { tripleToggleColor } from '@/utils/utils';
@ -30,65 +30,38 @@ import { tripleToggleColor } from '@/utils/utils';
interface ToolbarSearchProps {
total: 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({
total,
filtered,
hasCustomFilter,
query,
onChangeQuery,
path,
onChangePath,
head,
onChangeHead,
folderMode,
toggleFolderMode,
isVisible,
toggleVisible,
isOwned,
toggleOwned,
isEditor,
toggleEditor,
filterUser,
onChangeFilterUser,
resetFilter
}: ToolbarSearchProps) {
function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
const headMenu = useDropdown();
const userMenu = useDropdown();
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;
function handleChange(newValue: LocationHead | undefined) {
headMenu.hide();
onChangeHead(newValue);
setHead(newValue);
}
function handleToggleFolder() {
@ -157,7 +130,7 @@ function ToolbarSearch({
className='min-w-[15rem] text-sm mx-1 mb-1'
items={users}
value={filterUser}
onSelectValue={onChangeFilterUser}
onSelectValue={setFilterUser}
/>
</Dropdown>
</div>
@ -177,7 +150,7 @@ function ToolbarSearch({
noBorder
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
query={query}
onChangeQuery={onChangeQuery}
onChangeQuery={setQuery}
/>
{!folderMode ? (
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
@ -236,7 +209,7 @@ function ToolbarSearch({
noBorder
className='w-[4.5rem] sm:w-[5rem] flex-grow'
query={path}
onChangeQuery={onChangePath}
onChangeQuery={setPath}
/>
) : null}
</div>

View File

@ -13,45 +13,36 @@ import useWindowSize from '@/hooks/useWindowSize';
import { FolderNode, FolderTree } from '@/models/FolderTree';
import { HelpTopic } from '@/models/miscellaneous';
import { useFitHeight } from '@/stores/appLayout';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information } from '@/utils/labels';
interface ViewSideLocationProps {
folderTree: FolderTree;
isVisible: boolean;
subfolders: boolean;
activeLocation: string;
onChangeActiveLocation: (newValue: string) => void;
toggleFolderMode: () => void;
toggleSubfolders: () => void;
onRenameLocation: () => void;
}
function ViewSideLocation({
folderTree,
activeLocation,
subfolders,
isVisible,
onChangeActiveLocation,
toggleFolderMode,
toggleSubfolders,
onRenameLocation
}: ViewSideLocationProps) {
function ViewSideLocation({ folderTree, isVisible, onRenameLocation }: ViewSideLocationProps) {
const { user } = useAuth();
const { items } = useLibrary();
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 = (() => {
if (activeLocation.length <= 3 || !user) {
if (location.length <= 3 || !user) {
return false;
}
if (user.is_staff) {
return true;
}
const owned = items.filter(item => item.owner == user.id);
const located = owned.filter(
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
);
const located = owned.filter(item => item.location == location || item.location.startsWith(`${location}/`));
return located.length !== 0;
})();
@ -66,7 +57,7 @@ function ViewSideLocation({
.then(() => toast.success(information.pathReady))
.catch(console.error);
} else {
onChangeActiveLocation(target.getPath());
setLocation(target.getPath());
}
}
@ -97,7 +88,7 @@ function ViewSideLocation({
onClick={onRenameLocation}
/>
) : null}
{!!activeLocation ? (
{!!location ? (
<MiniButton
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
icon={<SubfoldersIcon value={subfolders} />}
@ -112,7 +103,7 @@ function ViewSideLocation({
</div>
</div>
<SelectLocation
value={activeLocation}
value={location}
folderTree={folderTree}
prefix={prefixes.folders_list}
onClick={handleClickFolder}

View File

@ -3,11 +3,11 @@
import clsx from 'clsx';
import { useState } from 'react';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { useMainHeight } from '@/stores/appLayout';
import { globals, storage } from '@/utils/constants';
import { usePreferencesStore } from '@/stores/preferences';
import { globals } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
import ViewConstituents from '../ViewConstituents';
@ -29,7 +29,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
const windowSize = useWindowSize();
const mainHeight = useMainHeight();
const [showList, setShowList] = useLocalStorage(storage.rseditShowList, true);
const showList = usePreferencesStore(state => state.showCstSideList);
const [toggleReset, setToggleReset] = useState(false);
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
@ -78,10 +79,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
activeCst={activeCst}
disabled={disabled}
modified={isModified}
showList={showList}
onSubmit={initiateSubmit}
onReset={() => setToggleReset(prev => !prev)}
onToggleList={() => setShowList(prev => !prev)}
/>
<div
tabIndex={-1}

View File

@ -20,6 +20,7 @@ import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { HelpTopic } from '@/models/miscellaneous';
import { IConstituenta } from '@/models/rsform';
import { usePreferencesStore } from '@/stores/preferences';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip, tooltips } from '@/utils/labels';
@ -29,25 +30,24 @@ interface ToolbarConstituentaProps {
activeCst?: IConstituenta;
disabled: boolean;
modified: boolean;
showList: boolean;
onSubmit: () => void;
onReset: () => void;
onToggleList: () => void;
}
function ToolbarConstituenta({
activeCst,
disabled,
modified,
showList,
onSubmit,
onReset,
onToggleList
onReset
}: ToolbarConstituentaProps) {
const controller = useRSEdit();
const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
return (
<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'
@ -104,7 +104,7 @@ function ToolbarConstituenta({
<MiniButton
title='Отображение списка конституент'
icon={showList ? <IconList size='1.25rem' className='icon-primary' /> : <IconListOff size='1.25rem' />}
onClick={onToggleList}
onClick={toggleList}
/>
{controller.isContentEditable ? (

View File

@ -13,14 +13,13 @@ import Overlay from '@/components/ui/Overlay';
import { useRSForm } from '@/context/RSFormContext';
import DlgShowAST from '@/dialogs/DlgShowAST';
import useCheckConstituenta from '@/hooks/useCheckConstituenta';
import useLocalStorage from '@/hooks/useLocalStorage';
import { HelpTopic } from '@/models/miscellaneous';
import { ConstituentaID, IConstituenta } from '@/models/rsform';
import { getDefinitionPrefix } from '@/models/rsformAPI';
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
import { TokenID } from '@/models/rslang';
import { usePreferencesStore } from '@/stores/preferences';
import { transformAST } from '@/utils/codemirror';
import { storage } from '@/utils/constants';
import { errors, labelTypification } from '@/utils/labels';
import ParsingResult from './ParsingResult';
@ -64,10 +63,10 @@ function EditorRSExpression({
const { resetParse } = parser;
const rsInput = useRef<ReactCodeMirrorRef>(null);
const showControls = usePreferencesStore(state => state.showExpressionControls);
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
const [expression, setExpression] = useState('');
const [showAST, setShowAST] = useState(false);
const [showControls, setShowControls] = useLocalStorage(storage.rseditShowControls, true);
useEffect(() => {
setIsModified(false);
@ -154,13 +153,7 @@ function EditorRSExpression({
<DlgShowAST expression={expression} syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)} />
) : null}
<ToolbarRSExpression
disabled={disabled}
showControls={showControls}
showAST={handleShowAST}
toggleControls={() => setShowControls(prev => !prev)}
showTypeGraph={onShowTypeGraph}
/>
<ToolbarRSExpression disabled={disabled} showAST={handleShowAST} showTypeGraph={onShowTypeGraph} />
<Overlay
position='top-[-0.5rem] right-1/2 translate-x-1/2'

View File

@ -3,24 +3,18 @@ import { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { useRSForm } from '@/context/RSFormContext';
import { usePreferencesStore } from '@/stores/preferences';
interface ToolbarRSExpressionProps {
disabled?: boolean;
showControls: boolean;
toggleControls: () => void;
showAST: (event: CProps.EventMouse) => void;
showTypeGraph: (event: CProps.EventMouse) => void;
}
function ToolbarRSExpression({
disabled,
showControls,
showTypeGraph,
toggleControls,
showAST
}: ToolbarRSExpressionProps) {
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
const model = useRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);
return (
<Overlay position='top-[-0.5rem] right-0' layer='z-pop' className='cc-icons'>

View File

@ -18,12 +18,12 @@ import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip';
import ValueIcon from '@/components/ui/ValueIcon';
import { useAccessMode } from '@/context/AccessModeContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useUsers } from '@/context/UsersContext';
import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
import { UserID, UserLevel } from '@/models/user';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { prefixes } from '@/utils/constants';
import { prompts } from '@/utils/labels';
@ -38,7 +38,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
const { accessLevel } = useAccessMode();
const intl = useIntl();
const router = useConceptNavigation();
const { setLocation, setFolderMode } = useConceptOptions();
const setLocation = useLibrarySearchStore(state => state.setLocation);
const ownerSelector = useDropdown();
const onSelectUser = useCallback(
@ -61,10 +61,9 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
return;
}
setLocation(item.location);
setFolderMode(true);
router.push(urls.library, event.ctrlKey || event.metaKey);
},
[setLocation, setFolderMode, item, router]
[setLocation, item, router]
);
if (!item) {

View File

@ -25,13 +25,12 @@ import { CProps } from '@/components/props';
import ToolbarGraphSelection from '@/components/select/ToolbarGraphSelection';
import Overlay from '@/components/ui/Overlay';
import DlgGraphParams from '@/dialogs/DlgGraphParams';
import useLocalStorage from '@/hooks/useLocalStorage';
import { GraphColoring, GraphFilterParams } from '@/models/miscellaneous';
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
import { isBasicConcept } from '@/models/rsformAPI';
import { useMainHeight } from '@/stores/appLayout';
import { useTermGraphStore } from '@/stores/termGraph';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { PARAMETER, storage } from '@/utils/constants';
import { PARAMETER } from '@/utils/constants';
import { errors } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext';
@ -62,29 +61,14 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
const { addSelectedNodes } = store.getState();
const [showParamsDialog, setShowParamsDialog] = useState(false);
const [filterParams, setFilterParams] = useLocalStorage<GraphFilterParams>(storage.rsgraphFilter, {
noHermits: true,
noTemplates: false,
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
});
const [coloring, setColoring] = useLocalStorage<GraphColoring>(storage.rsgraphColoring, 'type');
const filter = useTermGraphStore(state => state.filter);
const setFilter = useTermGraphStore(state => state.setFilter);
const coloring = useTermGraphStore(state => state.coloring);
const setColoring = useTermGraphStore(state => state.setColoring);
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 [isDragging, setIsDragging] = useState(false);
@ -139,7 +123,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
data: {
fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring),
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);
setEdges(newEdges);
}, [
controller.schema,
filteredGraph,
setNodes,
setEdges,
filterParams.noText,
controller.selected,
focusCst,
coloring
]);
}, [controller.schema, filteredGraph, setNodes, setEdges, filter.noText, controller.selected, focusCst, coloring]);
useEffect(() => {
setNeedReset(true);
}, [controller.schema, filterParams.noText, focusCst, coloring, flow.viewportInitialized]);
}, [controller.schema, filter.noText, focusCst, coloring, flow.viewportInitialized]);
useEffect(() => {
if (!controller.schema || !needReset || !flow.viewportInitialized) {
@ -198,7 +173,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
setTimeout(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, PARAMETER.minimalTimeout);
}, [toggleResetView, flow, focusCst, filterParams]);
}, [toggleResetView, flow, focusCst, filter]);
function handleSetSelected(newSelection: number[]) {
controller.setSelected(newSelection);
@ -220,10 +195,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
controller.promptDeleteCst();
}
function handleChangeParams(params: GraphFilterParams) {
setFilterParams(params);
}
function handleSaveImage() {
if (!controller.schema) {
return;
@ -283,10 +254,10 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
}
function handleFoldDerived() {
setFilterParams(prev => ({
...prev,
foldDerived: !prev.foldDerived
}));
setFilter({
...filter,
foldDerived: !filter.foldDerived
});
setTimeout(() => {
setToggleResetView(prev => !prev);
}, PARAMETER.graphRefreshDelay);
@ -325,17 +296,13 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
return (
<>
{showParamsDialog ? (
<DlgGraphParams
hideWindow={() => setShowParamsDialog(false)}
initial={filterParams}
onConfirm={handleChangeParams}
/>
<DlgGraphParams hideWindow={() => setShowParamsDialog(false)} initial={filter} onConfirm={setFilter} />
) : null}
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
<ToolbarTermGraph
noText={filterParams.noText}
foldDerived={filterParams.foldDerived}
noText={filter.noText}
foldDerived={filter.foldDerived}
showParamsDialog={() => setShowParamsDialog(true)}
onCreate={handleCreateCst}
onDelete={handleDeleteCst}
@ -343,10 +310,10 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
onSaveImage={handleSaveImage}
toggleFoldDerived={handleFoldDerived}
toggleNoText={() =>
setFilterParams(prev => ({
...prev,
noText: !prev.noText
}))
setFilter({
...filter,
noText: !filter.noText
})
}
/>
{!focusCst ? (
@ -367,19 +334,19 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
<ToolbarFocusedCst
center={focusCst}
reset={() => handleSetFocus(undefined)}
showInputs={filterParams.focusShowInputs}
showOutputs={filterParams.focusShowOutputs}
showInputs={filter.focusShowInputs}
showOutputs={filter.focusShowOutputs}
toggleShowInputs={() =>
setFilterParams(prev => ({
...prev,
focusShowInputs: !prev.focusShowInputs
}))
setFilter({
...filter,
focusShowInputs: !filter.focusShowInputs
})
}
toggleShowOutputs={() =>
setFilterParams(prev => ({
...prev,
focusShowOutputs: !prev.focusShowOutputs
}))
setFilter({
...filter,
focusShowOutputs: !filter.focusShowOutputs
})
}
/>
) : null}

View File

@ -7,13 +7,13 @@ import { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import useLocalStorage from '@/hooks/useLocalStorage';
import useWindowSize from '@/hooks/useWindowSize';
import { GraphColoring } from '@/models/miscellaneous';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { useFitHeight } from '@/stores/appLayout';
import { useTermGraphStore } from '@/stores/termGraph';
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
import { globals, PARAMETER, prefixes, storage } from '@/utils/constants';
import { globals, PARAMETER, prefixes } from '@/utils/constants';
interface ViewHiddenProps {
items: ConstituentaID[];
@ -29,7 +29,9 @@ interface ViewHiddenProps {
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
const windowSize = useWindowSize();
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 hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
@ -52,7 +54,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
noHover
title={!isFolded ? 'Свернуть' : 'Развернуть'}
icon={!isFolded ? <IconDropArrowUp size='1.25rem' /> : <IconDropArrow size='1.25rem' />}
onClick={() => setIsFolded(prev => !prev)}
onClick={toggleFolded}
/>
</Overlay>
<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 => {
const cst = schema.cstByID.get(cstID)!;
const adjustedColoring = coloringScheme === 'none' ? 'status' : coloringScheme;
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
return (
<button
@ -95,7 +96,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
type='button'
className='min-w-[3rem] rounded-md text-center select-none'
style={{
backgroundColor: colorBgGraphNode(cst, adjustedColoring),
backgroundColor: colorBgGraphNode(cst, coloringScheme),
...(localSelected.includes(cstID)
? {
outlineWidth: '2px',

View 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
};
}

View File

@ -1,20 +1,70 @@
import { flushSync } from 'react-dom';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { PARAMETER } from '@/utils/constants';
interface PreferencesStore {
darkMode: boolean;
toggleDarkMode: () => void;
showHelp: boolean;
adminMode: boolean;
toggleShowHelp: () => void;
adminMode: boolean;
toggleAdminMode: () => void;
libraryPagination: number;
setLibraryPagination: (value: number) => void;
showCstSideList: boolean;
toggleShowCstSideList: () => void;
showExpressionControls: boolean;
toggleShowExpressionControls: () => void;
}
export const usePreferencesStore = create<PreferencesStore>()(
persist(
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,
adminMode: false,
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,
@ -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 };
}

View 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'
}
)
);

View File

@ -108,25 +108,6 @@ export const external_urls = {
export const storage = {
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',
ossEdgeStraight: 'oss.edge_straight',
ossEdgeAnimate: 'oss.edge_animate',