Improve selectors and Refactor Icons

This commit is contained in:
IRBorisov 2024-05-02 17:04:18 +03:00
parent a3d62d60ec
commit f68484c834
30 changed files with 401 additions and 299 deletions

View File

@ -1,9 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { FaSquarePlus } from 'react-icons/fa6';
import { IoLibrary } from 'react-icons/io5';
import { IconManuals } from '@/components/Icons'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
@ -51,13 +49,13 @@ function Navigation() {
<NavigationButton <NavigationButton
text='Новая схема' text='Новая схема'
title='Создать новую схему' title='Создать новую схему'
icon={<FaSquarePlus size='1.5rem' />} icon={<IconNewItem2 size='1.5rem' />}
onClick={navigateCreateNew} onClick={navigateCreateNew}
/> />
<NavigationButton <NavigationButton
text='Библиотека' text='Библиотека'
title='Список схем' title='Список схем'
icon={<IoLibrary size='1.5rem' />} icon={<IconLibrary2 size='1.5rem' />}
onClick={navigateLibrary} onClick={navigateLibrary}
/> />
<NavigationButton text='Справка' title='Справочные материалы' icon={<IconManuals />} onClick={navigateHelp} /> <NavigationButton text='Справка' title='Справочные материалы' icon={<IconManuals />} onClick={navigateHelp} />

View File

@ -1,6 +1,14 @@
import { LuMoon, LuSun } from 'react-icons/lu'; import {
IconAdmin,
import { IconAdmin, IconAdminOff, IconDatabase, IconHelp, IconHelpOff, IconLogout, IconUser } from '@/components/Icons'; IconAdminOff,
IconDarkTheme,
IconDatabase,
IconHelp,
IconHelpOff,
IconLightTheme,
IconLogout,
IconUser
} from '@/components/Icons';
import { CProps } from '@/components/props'; 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';
@ -50,7 +58,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
/> />
<DropdownButton <DropdownButton
text={darkMode ? 'Тема: Темная' : 'Тема: Светлая'} text={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
icon={darkMode ? <LuMoon size='1rem' /> : <LuSun size='1rem' />} icon={darkMode ? <IconDarkTheme size='1rem' /> : <IconLightTheme size='1rem' />}
title='Переключение темы оформления' title='Переключение темы оформления'
onClick={handleToggleDarkMode} onClick={handleToggleDarkMode}
/> />

View File

@ -1,7 +1,6 @@
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { FaCircleUser } from 'react-icons/fa6';
import { IconLogin } from '@/components/Icons'; import { IconLogin, IconUser2 } from '@/components/Icons';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@ -40,7 +39,7 @@ function UserMenu() {
{user ? ( {user ? (
<AnimateFade key='nav_user_badge_profile' className='h-full'> <AnimateFade key='nav_user_badge_profile' className='h-full'>
<NavigationButton <NavigationButton
icon={<FaCircleUser size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />} icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={menu.toggle} onClick={menu.toggle}
/> />
</AnimateFade> </AnimateFade>

View File

@ -1,62 +1,86 @@
// Search new icons at https://reactsvgicons.com/ // Search new icons at https://reactsvgicons.com/
// ==== General actions =======
export { LuLogOut as IconLogout } from 'react-icons/lu'; export { LuLogOut as IconLogout } from 'react-icons/lu';
export { FiSave as IconSave } from 'react-icons/fi'; export { FiSave as IconSave } from 'react-icons/fi';
export { BiCheck as IconAccept } from 'react-icons/bi'; export { BiCheck as IconAccept } from 'react-icons/bi';
export { BiX as IconClose } from 'react-icons/bi';
export { BiX as IconRemove } from 'react-icons/bi'; export { BiX as IconRemove } from 'react-icons/bi';
export { BiTrash as IconDestroy } from 'react-icons/bi'; export { BiTrash as IconDestroy } from 'react-icons/bi';
export { BiReset as IconReset } from 'react-icons/bi'; export { BiReset as IconReset } from 'react-icons/bi';
export { BiPlusCircle as IconNewItem } from 'react-icons/bi'; export { LiaEdit as IconEdit } from 'react-icons/lia';
export { BiDuplicate as IconClone } from 'react-icons/bi'; export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu';
export { BiDownload as IconDownload } from 'react-icons/bi'; export { BiDownload as IconDownload } from 'react-icons/bi';
export { BiUpload as IconUpload } from 'react-icons/bi'; export { BiUpload as IconUpload } from 'react-icons/bi';
export { LiaEdit as IconEdit } from 'react-icons/lia'; export { BiCog as IconSettings } from 'react-icons/bi';
export { BiShareAlt as IconShare } from 'react-icons/bi';
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
export { BiDownArrowCircle as IconOpenList } from 'react-icons/bi';
// ===== UI elements =======
export { BiX as IconClose } from 'react-icons/bi';
export { LuChevronDown as IconDropArrow } from 'react-icons/lu';
export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu';
export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
export { LuSun as IconLightTheme } from 'react-icons/lu';
export { LuLightbulb as IconHelp } from 'react-icons/lu';
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { RiPushpinFill as IconPin } from 'react-icons/ri'; export { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
export { BiCog as IconSettings } from 'react-icons/bi'; export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
export { BiCaretUp as IconSortAsc } from 'react-icons/bi';
// ==== User status =======
export { LuUserCircle2 as IconUser } from 'react-icons/lu'; export { LuUserCircle2 as IconUser } from 'react-icons/lu';
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
export { LuCrown as IconOwner } from 'react-icons/lu'; export { LuCrown as IconOwner } from 'react-icons/lu';
export { TbMeteor as IconAdmin } from 'react-icons/tb'; export { TbMeteor as IconAdmin } from 'react-icons/tb';
export { TbMeteorOff as IconAdminOff } from 'react-icons/tb'; export { TbMeteorOff as IconAdminOff } from 'react-icons/tb';
export { LuGlasses as IconReader } from 'react-icons/lu'; export { LuGlasses as IconReader } from 'react-icons/lu';
export { FiBell as IconFollow } from 'react-icons/fi';
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
export { FaSortAmountDownAlt as IconSortText } from 'react-icons/fa';
export { LuChevronDown as IconDropArrow } from 'react-icons/lu';
export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu';
// ===== Domain entities =======
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu';
export { TbColumns as IconList } from 'react-icons/tb'; export { TbColumns as IconList } from 'react-icons/tb';
export { TbColumnsOff as IconListOff } from 'react-icons/tb'; export { TbColumnsOff as IconListOff } from 'react-icons/tb';
export { LuAtSign as IconTerm } from 'react-icons/lu';
export { LuSubscript as IconAlias } from 'react-icons/lu';
export { TbMathFunction as IconFormula } from 'react-icons/tb';
export { BiFontFamily as IconText } from 'react-icons/bi'; export { BiFontFamily as IconText } from 'react-icons/bi';
export { BiFont as IconTextOff } from 'react-icons/bi'; export { BiFont as IconTextOff } from 'react-icons/bi';
export { RiTreeLine as IconTree } from 'react-icons/ri'; export { RiTreeLine as IconTree } from 'react-icons/ri';
export { FaRegKeyboard as IconControls } from 'react-icons/fa6';
export { BiCheckShield as IconImmutable } from 'react-icons/bi';
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
// ===== Domain actions =====
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
export { BiDownvote as IconMoveDown } from 'react-icons/bi';
export { BiRightArrow as IconMoveRight } from 'react-icons/bi';
export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
export { FiBell as IconFollow } from 'react-icons/fi';
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu';
// ======== Graph UI =======
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi'; export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
export { BiExpand as IconGraphExpand } from 'react-icons/bi'; export { BiExpand as IconGraphExpand } from 'react-icons/bi';
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu'; export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi'; export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu'; export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { BiCheckShield as IconImmutable } from 'react-icons/bi';
export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
export { BiShareAlt as IconShare } from 'react-icons/bi';
export { LuLightbulb as IconHelp } from 'react-icons/lu';
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { BiFilterAlt as IconFilter } from 'react-icons/bi';
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
export { BiDownvote as IconMoveDown } from 'react-icons/bi';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu'; export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md'; export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
export { LuSparkles as IconClustering } from 'react-icons/lu'; export { LuSparkles as IconClustering } from 'react-icons/lu';
export { LuSparkle as IconClusteringOff } from 'react-icons/lu'; export { LuSparkle as IconClusteringOff } from 'react-icons/lu';
// ===== Custom elements ======
interface IconSVGProps { interface IconSVGProps {
viewBox: string; viewBox: string;
size?: string; size?: string;

View File

@ -1,4 +1,4 @@
import { IconFollow, IconImmutable, IconPublic } from '../Icons'; import { IconImmutable, IconPublic } from '../Icons';
function HelpLibrary() { function HelpLibrary() {
// prettier-ignore // prettier-ignore
@ -9,10 +9,6 @@ function HelpLibrary() {
<p>Фильтрация с помощью инструментов в верхней части страницы</p> <p>Фильтрация с помощью инструментов в верхней части страницы</p>
<p>Сортировка по клику на заголовок таблицы</p> <p>Сортировка по клику на заголовок таблицы</p>
<h2>Отображение статусов</h2> <h2>Отображение статусов</h2>
<div className='flex items-center gap-2'>
<IconFollow size='1rem'/>
<p><b>отслеживаемая</b> обозначает отслеживание схемы</p>
</div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<IconPublic size='1rem'/> <IconPublic size='1rem'/>
<p><b>общедоступная</b> отображает схему всем пользователям</p> <p><b>общедоступная</b> отображает схему всем пользователям</p>

View File

@ -11,7 +11,7 @@ import { describeConstituenta } from '@/utils/labels';
import ConstituentaBadge from '../info/ConstituentaBadge'; import ConstituentaBadge from '../info/ConstituentaBadge';
import FlexColumn from '../ui/FlexColumn'; import FlexColumn from '../ui/FlexColumn';
import SelectGraphToolbar from './SelectGraphToolbar'; import GraphSelectionToolbar from './GraphSelectionToolbar';
interface ConstituentaMultiPickerProps { interface ConstituentaMultiPickerProps {
id?: string; id?: string;
@ -80,7 +80,7 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
Выбраны {selected.length} из {schema?.items.length ?? 0} Выбраны {selected.length} из {schema?.items.length ?? 0}
</span> </span>
{schema ? ( {schema ? (
<SelectGraphToolbar <GraphSelectionToolbar
graph={schema.graph} graph={schema.graph}
core={schema.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)} core={schema.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
setSelected={setSelected} setSelected={setSelected}

View File

@ -14,13 +14,13 @@ import {
import { CProps } from '../props'; import { CProps } from '../props';
import MiniButton from '../ui/MiniButton'; import MiniButton from '../ui/MiniButton';
interface SelectGraphToolbarProps extends CProps.Styling { interface GraphSelectionToolbarProps extends CProps.Styling {
graph: Graph; graph: Graph;
core: number[]; core: number[];
setSelected: React.Dispatch<React.SetStateAction<number[]>>; setSelected: React.Dispatch<React.SetStateAction<number[]>>;
} }
function SelectGraphToolbar({ className, graph, core, setSelected, ...restProps }: SelectGraphToolbarProps) { function GraphSelectionToolbar({ className, graph, core, setSelected, ...restProps }: GraphSelectionToolbarProps) {
return ( return (
<div className={clsx('cc-icons', className)} {...restProps}> <div className={clsx('cc-icons', className)} {...restProps}>
<MiniButton <MiniButton
@ -62,4 +62,4 @@ function SelectGraphToolbar({ className, graph, core, setSelected, ...restProps
); );
} }
export default SelectGraphToolbar; export default GraphSelectionToolbar;

View File

@ -0,0 +1,93 @@
'use client';
import { useCallback } from 'react';
import { IconFilter, IconFollow, IconImmutable, IconOwner, IconPublic } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown';
import SelectorButton from '@/components/ui/SelectorButton';
import { useAuth } from '@/context/AuthContext';
import useDropdown from '@/hooks/useDropdown';
import useWindowSize from '@/hooks/useWindowSize';
import { LibraryFilterStrategy } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants';
import { describeLibraryFilter, labelLibraryFilter } from '@/utils/labels';
import DropdownButton from '../ui/DropdownButton';
function StrategyIcon(strategy: LibraryFilterStrategy, size: string, color?: string) {
switch (strategy) {
case LibraryFilterStrategy.MANUAL:
return <IconFilter size={size} className={color} />;
case LibraryFilterStrategy.CANONICAL:
return <IconImmutable size={size} className={color} />;
case LibraryFilterStrategy.COMMON:
return <IconPublic size={size} className={color} />;
case LibraryFilterStrategy.OWNED:
return <IconOwner size={size} className={color} />;
case LibraryFilterStrategy.SUBSCRIBE:
return <IconFollow size={size} className={color} />;
}
}
interface SelectFilterStrategyProps {
value: LibraryFilterStrategy;
onChange: (value: LibraryFilterStrategy) => void;
}
function SelectFilterStrategy({ value, onChange }: SelectFilterStrategyProps) {
const menu = useDropdown();
const { user } = useAuth();
const size = useWindowSize();
const handleChange = useCallback(
(newValue: LibraryFilterStrategy) => {
menu.hide();
onChange(newValue);
},
[menu, onChange]
);
function isStrategyDisabled(strategy: LibraryFilterStrategy): boolean {
if (strategy === LibraryFilterStrategy.SUBSCRIBE || strategy === LibraryFilterStrategy.OWNED) {
return !user;
} else {
return false;
}
}
return (
<div ref={menu.ref} className='h-full text-right'>
<SelectorButton
transparent
tabIndex={-1}
title={describeLibraryFilter(value)}
hideTitle={menu.isOpen}
className='h-full'
icon={StrategyIcon(value, '1rem', value !== LibraryFilterStrategy.MANUAL ? 'icon-primary' : '')}
text={size.isSmall ? undefined : labelLibraryFilter(value)}
onClick={menu.toggle}
/>
<Dropdown isOpen={menu.isOpen}>
{Object.values(LibraryFilterStrategy).map((enumValue, index) => {
const strategy = enumValue as LibraryFilterStrategy;
return (
<DropdownButton
className='w-[10rem]'
key={`${prefixes.library_filters_list}${index}`}
onClick={() => handleChange(strategy)}
title={describeLibraryFilter(strategy)}
disabled={isStrategyDisabled(strategy)}
>
<div className='inline-flex items-center gap-3'>
{StrategyIcon(strategy, '1rem')}
{labelLibraryFilter(strategy)}
</div>
</DropdownButton>
);
})}
</Dropdown>
</div>
);
}
export default SelectFilterStrategy;

View File

@ -0,0 +1,91 @@
'use client';
import { useCallback } from 'react';
import Dropdown from '@/components/ui/Dropdown';
import SelectorButton from '@/components/ui/SelectorButton';
import useDropdown from '@/hooks/useDropdown';
import useWindowSize from '@/hooks/useWindowSize';
import { DependencyMode } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants';
import { describeCstSource, labelCstSource } from '@/utils/labels';
import {
IconGraphCollapse,
IconGraphExpand,
IconGraphInputs,
IconGraphOutputs,
IconSettings,
IconText
} from '../Icons';
import DropdownButton from '../ui/DropdownButton';
function DependencyIcon(mode: DependencyMode, size: string, color?: string) {
switch (mode) {
case DependencyMode.ALL:
return <IconSettings size={size} className={color} />;
case DependencyMode.EXPRESSION:
return <IconText size={size} className={color} />;
case DependencyMode.OUTPUTS:
return <IconGraphOutputs size={size} className={color} />;
case DependencyMode.INPUTS:
return <IconGraphInputs size={size} className={color} />;
case DependencyMode.EXPAND_OUTPUTS:
return <IconGraphExpand size={size} className={color} />;
case DependencyMode.EXPAND_INPUTS:
return <IconGraphCollapse size={size} className={color} />;
}
}
interface SelectGraphFilterProps {
value: DependencyMode;
onChange: (value: DependencyMode) => void;
}
function SelectGraphFilter({ value, onChange }: SelectGraphFilterProps) {
const menu = useDropdown();
const size = useWindowSize();
const handleChange = useCallback(
(newValue: DependencyMode) => {
menu.hide();
onChange(newValue);
},
[menu, onChange]
);
return (
<div ref={menu.ref}>
<SelectorButton
transparent
tabIndex={-1}
title='Настройка фильтрации по графу термов'
hideTitle={menu.isOpen}
className='h-full pr-2'
icon={DependencyIcon(value, '1rem', value !== DependencyMode.ALL ? 'icon-primary' : '')}
text={size.isSmall ? undefined : labelCstSource(value)}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen}>
{Object.values(DependencyMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {
const source = value as DependencyMode;
return (
<DropdownButton
className='w-[18rem]'
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleChange(source)}
>
<div className='inline-flex items-center gap-1'>
{DependencyIcon(source, '1rem')}
<b>{labelCstSource(source)}:</b> {describeCstSource(source)}
</div>
</DropdownButton>
);
})}
</Dropdown>
</div>
);
}
export default SelectGraphFilter;

View File

@ -0,0 +1,82 @@
'use client';
import { useCallback } from 'react';
import Dropdown from '@/components/ui/Dropdown';
import SelectorButton from '@/components/ui/SelectorButton';
import useDropdown from '@/hooks/useDropdown';
import useWindowSize from '@/hooks/useWindowSize';
import { CstMatchMode } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants';
import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
import { IconAlias, IconTerm, IconFilter, IconFormula, IconText } from '../Icons';
import DropdownButton from '../ui/DropdownButton';
function MatchModeIcon(mode: CstMatchMode, size: string, color?: string) {
switch (mode) {
case CstMatchMode.ALL:
return <IconFilter size={size} className={color} />;
case CstMatchMode.TEXT:
return <IconText size={size} className={color} />;
case CstMatchMode.EXPR:
return <IconFormula size={size} className={color} />;
case CstMatchMode.TERM:
return <IconTerm size={size} className={color} />;
case CstMatchMode.NAME:
return <IconAlias size={size} className={color} />;
}
}
interface SelectMatchModeProps {
value: CstMatchMode;
onChange: (value: CstMatchMode) => void;
}
function SelectMatchMode({ value, onChange }: SelectMatchModeProps) {
const menu = useDropdown();
const size = useWindowSize();
const handleChange = useCallback(
(newValue: CstMatchMode) => {
menu.hide();
onChange(newValue);
},
[menu, onChange]
);
return (
<div ref={menu.ref}>
<SelectorButton
transparent
tabIndex={-1}
title='Настройка фильтрации по проверяемым атрибутам'
hideTitle={menu.isOpen}
className='h-full pr-2'
icon={MatchModeIcon(value, '1rem', value !== CstMatchMode.ALL ? 'icon-primary' : '')}
text={size.isSmall ? undefined : labelCstMatchMode(value)}
onClick={menu.toggle}
/>
<Dropdown stretchLeft isOpen={menu.isOpen}>
{Object.values(CstMatchMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {
const matchMode = value as CstMatchMode;
return (
<DropdownButton
className='w-[20rem]'
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleChange(matchMode)}
>
<div className='inline-flex items-center gap-1'>
{MatchModeIcon(matchMode, '1rem')}
<b>{labelCstMatchMode(matchMode)}:</b> {describeCstMatchMode(matchMode)}
</div>
</DropdownButton>
);
})}
</Dropdown>
</div>
);
}
export default SelectMatchMode;

View File

@ -1,5 +1,6 @@
import { Column } from '@tanstack/react-table'; import { Column } from '@tanstack/react-table';
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
import { IconSortAsc, IconSortDesc } from '@/components/Icons';
interface SortingIconProps<TData> { interface SortingIconProps<TData> {
column: Column<TData>; column: Column<TData>;
@ -9,9 +10,9 @@ function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return ( return (
<> <>
{{ {{
desc: <BiCaretDown size='1rem' />, desc: <IconSortDesc size='1rem' />,
asc: <BiCaretUp size='1rem' /> asc: <IconSortAsc size='1rem' />
}[column.getIsSorted() as string] ?? <BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />} }[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 hover:opacity-50' />}
</> </>
); );
} }

View File

@ -1,5 +1,4 @@
import { BiSearchAlt2 } from 'react-icons/bi'; import { IconSearch } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import Overlay from './Overlay'; import Overlay from './Overlay';
import TextInput from './TextInput'; import TextInput from './TextInput';
@ -15,7 +14,7 @@ function SearchBar({ id, value, onChange, noBorder, ...restProps }: SearchBarPro
return ( return (
<div {...restProps}> <div {...restProps}>
<Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'> <Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
<BiSearchAlt2 size='1.25rem' /> <IconSearch size='1.25rem' />
</Overlay> </Overlay>
<TextInput <TextInput
id={id} id={id}

View File

@ -13,7 +13,7 @@ import {
postNewRSForm postNewRSForm
} from '@/app/backendAPI'; } from '@/app/backendAPI';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI'; import { matchLibraryItem } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCloneData, IRSFormCreateData, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormCloneData, IRSFormCreateData, IRSFormData } from '@/models/rsform';
@ -31,13 +31,13 @@ interface ILibraryContext {
setError: (error: ErrorData) => void; setError: (error: ErrorData) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[]; applyFilter: (params: ILibraryFilter) => ILibraryItem[];
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void; retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void; createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
cloneItem: (target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void; cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
destroyItem: (target: number, callback?: () => void) => void; destroyItem: (target: LibraryItemID, callback?: () => void) => void;
localUpdateItem: (data: ILibraryItem) => void; localUpdateItem: (data: ILibraryItem) => void;
localUpdateTimestamp: (target: number) => void; localUpdateTimestamp: (target: LibraryItemID) => void;
} }
const LibraryContext = createContext<ILibraryContext | null>(null); const LibraryContext = createContext<ILibraryContext | null>(null);
@ -79,9 +79,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
if (params.is_subscribed !== undefined) { if (params.is_subscribed !== undefined) {
result = result.filter(item => user?.subscriptions.includes(item.id)); result = result.filter(item => user?.subscriptions.includes(item.id));
} }
if (params.is_personal !== undefined) {
result = result.filter(item => user?.subscriptions.includes(item.id) || item.owner === user?.id);
}
if (params.query) { if (params.query) {
result = result.filter(item => matchLibraryItem(item, params.query!)); result = result.filter(item => matchLibraryItem(item, params.query!));
} }
@ -91,7 +88,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
); );
const retrieveTemplate = useCallback( const retrieveTemplate = useCallback(
(templateID: number, callback: (schema: IRSForm) => void) => { (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => {
const cached = cachedTemplates.find(schema => schema.id == templateID); const cached = cachedTemplates.find(schema => schema.id == templateID);
if (cached) { if (cached) {
callback(cached); callback(cached);
@ -166,7 +163,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
); );
const localUpdateTimestamp = useCallback( const localUpdateTimestamp = useCallback(
(target: number) => { (target: LibraryItemID) => {
const libraryItem = items.find(item => item.id === target); const libraryItem = items.find(item => item.id === target);
if (libraryItem) { if (libraryItem) {
libraryItem.time_update = Date(); libraryItem.time_update = Date();
@ -196,7 +193,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
); );
const destroyItem = useCallback( const destroyItem = useCallback(
(target: number, callback?: () => void) => { (target: LibraryItemID, callback?: () => void) => {
setError(undefined); setError(undefined);
deleteLibraryItem(String(target), { deleteLibraryItem(String(target), {
showError: true, showError: true,
@ -218,7 +215,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
); );
const cloneItem = useCallback( const cloneItem = useCallback(
(target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => { (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => {
if (!user) { if (!user) {
return; return;
} }

View File

@ -2,10 +2,10 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DataCallback, getProfile, patchProfile } from '@/app/backendAPI';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import { IUserProfile } from '@/models/library'; import { IUserProfile } from '@/models/library';
import { IUserUpdateData } from '@/models/library'; import { IUserUpdateData } from '@/models/library';
import { DataCallback, getProfile, patchProfile } from '@/app/backendAPI';
import { useUsers } from './UsersContext'; import { useUsers } from './UsersContext';

View File

@ -2,9 +2,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { BiChevronsDown, BiLeftArrow, BiRightArrow } from 'react-icons/bi';
import { IconAccept, IconRemove } from '@/components/Icons'; import { IconAccept, IconMoveDown, IconMoveLeft, IconMoveRight, IconRemove } from '@/components/Icons';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
import SelectGrammeme from '@/components/select/SelectGrammeme'; import SelectGrammeme from '@/components/select/SelectGrammeme';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
@ -159,14 +158,14 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton <MiniButton
noHover noHover
title='Определить граммемы' title='Определить граммемы'
icon={<BiRightArrow size='1.25rem' className='icon-primary' />} icon={<IconMoveRight size='1.25rem' className='icon-primary' />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleParse} onClick={handleParse}
/> />
<MiniButton <MiniButton
noHover noHover
title='Генерировать словоформу' title='Генерировать словоформу'
icon={<BiLeftArrow size='1.25rem' className='icon-primary' />} icon={<IconMoveLeft size='1.25rem' className='icon-primary' />}
disabled={textProcessor.loading || inputGrams.length == 0} disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect} onClick={handleInflect}
/> />
@ -190,7 +189,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton <MiniButton
noHover noHover
title='Генерировать стандартные словоформы' title='Генерировать стандартные словоформы'
icon={<BiChevronsDown size='1.5rem' className='icon-primary' />} icon={<IconMoveDown size='1.5rem' className='icon-primary' />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleGenerateLexeme} onClick={handleGenerateLexeme}
/> />

View File

@ -74,7 +74,6 @@ export enum CstMatchMode {
*/ */
export interface ILibraryFilter { export interface ILibraryFilter {
query?: string; query?: string;
is_personal?: boolean;
is_owned?: boolean; is_owned?: boolean;
is_common?: boolean; is_common?: boolean;
is_canonical?: boolean; is_canonical?: boolean;
@ -86,7 +85,6 @@ export interface ILibraryFilter {
*/ */
export enum LibraryFilterStrategy { export enum LibraryFilterStrategy {
MANUAL = 'manual', MANUAL = 'manual',
PERSONAL = 'personal',
COMMON = 'common', COMMON = 'common',
SUBSCRIBE = 'subscribe', SUBSCRIBE = 'subscribe',
CANONICAL = 'canonical', CANONICAL = 'canonical',

View File

@ -51,7 +51,6 @@ export function filterFromStrategy(strategy: LibraryFilterStrategy): ILibraryFil
case LibraryFilterStrategy.MANUAL: return {}; case LibraryFilterStrategy.MANUAL: return {};
case LibraryFilterStrategy.COMMON: return { is_common: true }; case LibraryFilterStrategy.COMMON: return { is_common: true };
case LibraryFilterStrategy.CANONICAL: return { is_canonical: true }; case LibraryFilterStrategy.CANONICAL: return { is_canonical: true };
case LibraryFilterStrategy.PERSONAL: return { is_personal: true };
case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true }; case LibraryFilterStrategy.SUBSCRIBE: return { is_subscribed: true };
case LibraryFilterStrategy.OWNED: return { is_owned: true }; case LibraryFilterStrategy.OWNED: return { is_owned: true };
} }

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconFollow, IconImmutable, IconPublic } from '@/components/Icons'; import { IconImmutable, IconPublic } from '@/components/Icons';
import { ICurrentUser, ILibraryItem } from '@/models/library'; import { ICurrentUser, ILibraryItem } from '@/models/library';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
@ -9,14 +9,9 @@ interface ItemIconsProps {
item: ILibraryItem; item: ILibraryItem;
} }
function ItemIcons({ user, item }: ItemIconsProps) { function ItemIcons({ item }: ItemIconsProps) {
return ( return (
<div className={clsx('min-w-[3.3rem]', 'inline-flex gap-1 align-middle')} id={`${prefixes.library_list}${item.id}`}> <div className={clsx('min-w-[2.2rem]', 'inline-flex gap-1 align-middle')} id={`${prefixes.library_list}${item.id}`}>
{user && user.subscriptions.includes(item.id) ? (
<span title='Отслеживаемая'>
<IconFollow size='1rem' />
</span>
) : null}
{item.is_common ? ( {item.is_common ? (
<span title='Общедоступная'> <span title='Общедоступная'>
<IconPublic size='1rem' /> <IconPublic size='1rem' />

View File

@ -1,75 +0,0 @@
'use client';
import { useCallback } from 'react';
import { IconFilter } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown';
import DropdownCheckbox from '@/components/ui/DropdownCheckbox';
import SelectorButton from '@/components/ui/SelectorButton';
import { useAuth } from '@/context/AuthContext';
import useDropdown from '@/hooks/useDropdown';
import { LibraryFilterStrategy } from '@/models/miscellaneous';
import { prefixes } from '@/utils/constants';
import { describeLibraryFilter, labelLibraryFilter } from '@/utils/labels';
interface PickerStrategyProps {
value: LibraryFilterStrategy;
onChange: (value: LibraryFilterStrategy) => void;
}
function PickerStrategy({ value, onChange }: PickerStrategyProps) {
const strategyMenu = useDropdown();
const { user } = useAuth();
const handleChange = useCallback(
(newValue: LibraryFilterStrategy) => {
strategyMenu.hide();
onChange(newValue);
},
[strategyMenu, onChange]
);
function isStrategyDisabled(strategy: LibraryFilterStrategy): boolean {
if (
strategy === LibraryFilterStrategy.PERSONAL ||
strategy === LibraryFilterStrategy.SUBSCRIBE ||
strategy === LibraryFilterStrategy.OWNED
) {
return !user;
} else {
return false;
}
}
return (
<div ref={strategyMenu.ref} className='h-full text-right'>
<SelectorButton
transparent
tabIndex={-1}
title='Список фильтров'
hideTitle={strategyMenu.isOpen}
className='h-full'
icon={<IconFilter size='1.25rem' />}
text={labelLibraryFilter(value)}
onClick={strategyMenu.toggle}
/>
<Dropdown isOpen={strategyMenu.isOpen}>
{Object.values(LibraryFilterStrategy).map((enumValue, index) => {
const strategy = enumValue as LibraryFilterStrategy;
return (
<DropdownCheckbox
key={`${prefixes.library_filters_list}${index}`}
value={value === strategy}
setValue={() => handleChange(strategy)}
label={labelLibraryFilter(strategy)}
title={describeLibraryFilter(strategy)}
disabled={isStrategyDisabled(strategy)}
/>
);
})}
</Dropdown>
</div>
);
}
export default PickerStrategy;

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback, useMemo } from 'react';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
@ -9,7 +9,7 @@ import { useConceptNavigation } from '@/context/NavigationContext';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { LibraryFilterStrategy } from '@/models/miscellaneous'; import { LibraryFilterStrategy } from '@/models/miscellaneous';
import PickerStrategy from './PickerStrategy'; import SelectFilterStrategy from '../../components/select/SelectFilterStrategy';
interface SearchPanelProps { interface SearchPanelProps {
total: number; total: number;
@ -30,8 +30,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
is_owned: prev.is_owned, is_owned: prev.is_owned,
is_common: prev.is_common, is_common: prev.is_common,
is_canonical: prev.is_canonical, is_canonical: prev.is_canonical,
is_subscribed: prev.is_subscribed, is_subscribed: prev.is_subscribed
is_personal: prev.is_personal
})); }));
} }
@ -44,6 +43,11 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
[strategy, router] [strategy, router]
); );
const selectStrategy = useMemo(
() => <SelectFilterStrategy value={strategy} onChange={handleChangeStrategy} />,
[strategy, handleChangeStrategy]
);
return ( return (
<div <div
className={clsx( className={clsx(
@ -68,7 +72,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
{filtered} из {total} {filtered} из {total}
</span> </span>
</div> </div>
<PickerStrategy value={strategy} onChange={handleChangeStrategy} /> {selectStrategy}
<SearchBar <SearchBar
id='library_search' id='library_search'
noBorder noBorder

View File

@ -10,7 +10,6 @@ import { CProps } from '@/components/props';
import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, VisibilityState } from '@/components/ui/DataTable';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { useAuth } from '@/context/AuthContext';
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 useLocalStorage from '@/hooks/useLocalStorage';
@ -31,7 +30,6 @@ const columnHelper = createColumnHelper<ILibraryItem>();
function ViewLibrary({ items, resetQuery }: ViewLibraryProps) { function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
const { user } = useAuth();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50); const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>(storage.libraryPagination, 50);
@ -57,7 +55,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
size: 60, size: 60,
minSize: 60, minSize: 60,
maxSize: 60, maxSize: 60,
cell: props => <ItemIcons item={props.row.original} user={user} /> cell: props => <ItemIcons item={props.row.original} />
}), }),
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
id: 'alias', id: 'alias',
@ -108,7 +106,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
sortDescFirst: true sortDescFirst: true
}) })
], ],
[intl, getUserLabel, user, windowSize] [intl, getUserLabel, windowSize]
); );
return ( return (

View File

@ -3,8 +3,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { RiMenuFoldFill, RiMenuUnfoldFill } from 'react-icons/ri';
import { IconMenuFold, IconMenuUnfold } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
@ -50,7 +50,7 @@ function TopicsListDropDown({ activeTopic, onChangeTopic }: TopicsListDropDownPr
tabIndex={-1} tabIndex={-1}
title='Список тем' title='Список тем'
hideTitle={menu.isOpen} hideTitle={menu.isOpen}
icon={!menu.isOpen ? <RiMenuUnfoldFill size='1.25rem' /> : <RiMenuFoldFill size='1.25rem' />} icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
className='w-[3rem] h-7' className='w-[3rem] h-7'
onClick={menu.toggle} onClick={menu.toggle}
/> />

View File

@ -3,10 +3,9 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { FaRegKeyboard } from 'react-icons/fa6';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconList, IconListOff, IconText, IconTextOff, IconTree } from '@/components/Icons'; import { IconControls, IconList, IconListOff, IconText, IconTextOff, IconTree } from '@/components/Icons';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
import { RSTextWrapper } from '@/components/RSInput/textEditing'; import { RSTextWrapper } from '@/components/RSInput/textEditing';
@ -182,7 +181,7 @@ function EditorRSExpression({
{!disabled || model.processing ? ( {!disabled || model.processing ? (
<MiniButton <MiniButton
title='Отображение специальной клавиатуры' title='Отображение специальной клавиатуры'
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'icon-primary' : ''} />} icon={<IconControls size='1.25rem' className={showControls ? 'icon-primary' : ''} />}
onClick={() => setShowControls(prev => !prev)} onClick={() => setShowControls(prev => !prev)}
/> />
) : null} ) : null}

View File

@ -1,6 +1,4 @@
import { BiDownArrowCircle } from 'react-icons/bi'; import { IconClone, IconDestroy, IconMoveDown, IconMoveUp, IconNewItem, IconOpenList } from '@/components/Icons';
import { IconClone, IconDestroy, IconMoveDown, IconMoveUp, IconNewItem } from '@/components/Icons';
import BadgeHelp from '@/components/man/BadgeHelp'; import BadgeHelp from '@/components/man/BadgeHelp';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
@ -49,7 +47,7 @@ function RSListToolbar() {
<MiniButton <MiniButton
title='Добавить пустую конституенту' title='Добавить пустую конституенту'
hideTitle={insertMenu.isOpen} hideTitle={insertMenu.isOpen}
icon={<BiDownArrowCircle size='1.25rem' className='icon-green' />} icon={<IconOpenList size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={insertMenu.toggle} onClick={insertMenu.toggle}
/> />

View File

@ -8,7 +8,7 @@ import { useDebounce } from 'use-debounce';
import InfoConstituenta from '@/components/info/InfoConstituenta'; import InfoConstituenta from '@/components/info/InfoConstituenta';
import SelectedCounter from '@/components/info/SelectedCounter'; import SelectedCounter from '@/components/info/SelectedCounter';
import SelectGraphToolbar from '@/components/select/SelectGraphToolbar'; import GraphSelectionToolbar from '@/components/select/GraphSelectionToolbar';
import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI'; import { GraphCanvasRef, GraphEdge, GraphLayout, GraphNode } from '@/components/ui/GraphUI';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
@ -311,7 +311,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
} }
/> />
{!focusCst ? ( {!focusCst ? (
<SelectGraphToolbar <GraphSelectionToolbar
graph={controller.schema!.graph} graph={controller.schema!.graph}
core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)} core={controller.schema!.items.filter(cst => isBasicConcept(cst.cst_type)).map(cst => cst.id)}
setSelected={controller.setSelected} setSelected={controller.setSelected}

View File

@ -16,7 +16,7 @@ import {
IconReader, IconReader,
IconReplace, IconReplace,
IconShare, IconShare,
IconSortText, IconSortList,
IconUpload IconUpload
} from '@/components/Icons'; } from '@/components/Icons';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
@ -226,7 +226,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
className='border-t-2' className='border-t-2'
text='Упорядочить список' text='Упорядочить список'
title='Упорядочить список конституент исходя из логики типов и связей конституент' title='Упорядочить список конституент исходя из логики типов и связей конституент'
icon={<IconSortText size='1rem' className='icon-primary' />} icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing} disabled={!controller.isContentEditable || controller.isProcessing}
onClick={handleRestoreOrder} onClick={handleRestoreOrder}
/> />

View File

@ -1,29 +1,17 @@
'use client'; 'use client';
import { useCallback, useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useMemo, useState } from 'react';
import { import SelectGraphFilter from '@/components/select/SelectGraphFilter';
IconFilter, import SelectMatchMode from '@/components/select/SelectMatchMode';
IconGraphCollapse,
IconGraphExpand,
IconGraphInputs,
IconGraphOutputs,
IconSettings,
IconText
} from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import SelectorButton from '@/components/ui/SelectorButton';
import useDropdown from '@/hooks/useDropdown';
import useLocalStorage from '@/hooks/useLocalStorage'; import useLocalStorage from '@/hooks/useLocalStorage';
import { CstMatchMode, DependencyMode } from '@/models/miscellaneous'; import { CstMatchMode, DependencyMode } from '@/models/miscellaneous';
import { applyGraphFilter } from '@/models/miscellaneousAPI'; import { applyGraphFilter } from '@/models/miscellaneousAPI';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform'; import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { createMockConstituenta, matchConstituenta } from '@/models/rsformAPI'; import { createMockConstituenta, matchConstituenta } from '@/models/rsformAPI';
import { extractGlobals } from '@/models/rslangAPI'; import { extractGlobals } from '@/models/rslangAPI';
import { prefixes, storage } from '@/utils/constants'; import { storage } from '@/utils/constants';
import { describeCstMatchMode, describeCstSource, labelCstMatchMode, labelCstSource } from '@/utils/labels';
interface ConstituentsSearchProps { interface ConstituentsSearchProps {
schema?: IRSForm; schema?: IRSForm;
@ -32,31 +20,11 @@ interface ConstituentsSearchProps {
setFiltered: React.Dispatch<React.SetStateAction<IConstituenta[]>>; setFiltered: React.Dispatch<React.SetStateAction<IConstituenta[]>>;
} }
function DependencyIcon(mode: DependencyMode, size: string) {
switch (mode) {
case DependencyMode.ALL:
return <IconSettings size={size} />;
case DependencyMode.EXPRESSION:
return <IconText size={size} />;
case DependencyMode.OUTPUTS:
return <IconGraphOutputs size={size} />;
case DependencyMode.INPUTS:
return <IconGraphInputs size={size} />;
case DependencyMode.EXPAND_OUTPUTS:
return <IconGraphExpand size={size} />;
case DependencyMode.EXPAND_INPUTS:
return <IconGraphCollapse size={size} />;
}
}
function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }: ConstituentsSearchProps) { function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }: ConstituentsSearchProps) {
const [filterMatch, setFilterMatch] = useLocalStorage(storage.cstFilterMatch, CstMatchMode.ALL); const [filterMatch, setFilterMatch] = useLocalStorage(storage.cstFilterMatch, CstMatchMode.ALL);
const [filterSource, setFilterSource] = useLocalStorage(storage.cstFilterGraph, DependencyMode.ALL); const [filterSource, setFilterSource] = useLocalStorage(storage.cstFilterGraph, DependencyMode.ALL);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
const matchModeMenu = useDropdown();
const sourceMenu = useDropdown();
useLayoutEffect(() => { useLayoutEffect(() => {
if (!schema || schema.items.length === 0) { if (!schema || schema.items.length === 0) {
setFiltered([]); setFiltered([]);
@ -82,20 +50,14 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
setFiltered(result); setFiltered(result);
}, [filterText, setFiltered, filterSource, activeExpression, schema?.items, schema, filterMatch, activeID]); }, [filterText, setFiltered, filterSource, activeExpression, schema?.items, schema, filterMatch, activeID]);
const handleMatchModeChange = useCallback( const selectGraph = useMemo(
(newValue: CstMatchMode) => { () => <SelectGraphFilter value={filterSource} onChange={newValue => setFilterSource(newValue)} />,
matchModeMenu.hide(); [filterSource, setFilterSource]
setFilterMatch(newValue);
},
[matchModeMenu, setFilterMatch]
); );
const handleSourceChange = useCallback( const selectMatchMode = useMemo(
(newValue: DependencyMode) => { () => <SelectMatchMode value={filterMatch} onChange={newValue => setFilterMatch(newValue)} />,
sourceMenu.hide(); [filterMatch, setFilterMatch]
setFilterSource(newValue);
},
[sourceMenu, setFilterSource]
); );
return ( return (
@ -107,69 +69,8 @@ function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }:
value={filterText} value={filterText}
onChange={setFilterText} onChange={setFilterText}
/> />
{selectMatchMode}
<div ref={matchModeMenu.ref}> {selectGraph}
<SelectorButton
transparent
tabIndex={-1}
title='Настройка атрибутов для фильтрации'
hideTitle={matchModeMenu.isOpen}
className='h-full'
icon={<IconFilter size='1.25rem' />}
text={labelCstMatchMode(filterMatch)}
onClick={matchModeMenu.toggle}
/>
<Dropdown stretchLeft isOpen={matchModeMenu.isOpen}>
{Object.values(CstMatchMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {
const matchMode = value as CstMatchMode;
return (
<DropdownButton
className='w-[22rem]'
key={`${prefixes.cst_match_mode_list}${index}`}
onClick={() => handleMatchModeChange(matchMode)}
>
<p>
<b>{labelCstMatchMode(matchMode)}:</b> {describeCstMatchMode(matchMode)}
</p>
</DropdownButton>
);
})}
</Dropdown>
</div>
<div ref={sourceMenu.ref}>
<SelectorButton
transparent
tabIndex={-1}
title='Настройка фильтрации по графу термов'
hideTitle={sourceMenu.isOpen}
className='h-full pr-2'
icon={DependencyIcon(filterSource, '1.25rem')}
text={labelCstSource(filterSource)}
onClick={sourceMenu.toggle}
/>
<Dropdown stretchLeft isOpen={sourceMenu.isOpen}>
{Object.values(DependencyMode)
.filter(value => !isNaN(Number(value)))
.map((value, index) => {
const source = value as DependencyMode;
return (
<DropdownButton
className='w-[18rem]'
key={`${prefixes.cst_source_list}${index}`}
onClick={() => handleSourceChange(source)}
>
<div className='inline-flex items-center gap-1'>
{DependencyIcon(source, '1.25rem')}
<b>{labelCstSource(source)}:</b> {describeCstSource(source)}
</div>
</DropdownButton>
);
})}
</Dropdown>
</div>
</div> </div>
); );
} }

View File

@ -49,7 +49,7 @@ function UserTabs() {
onClick={() => setShowSubs(prev => !prev)} onClick={() => setShowSubs(prev => !prev)}
/> />
</Overlay> </Overlay>
<h1 className='mb-4'>Учетные данные пользователя</h1> <h1 className='mb-4 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'> <div className='flex py-2'>
<EditorProfile /> <EditorProfile />
<EditorPassword /> <EditorPassword />

View File

@ -36,8 +36,8 @@ function ViewSubscriptions({ items }: ViewSubscriptionsProps) {
id: 'title', id: 'title',
header: 'Название', header: 'Название',
minSize: 200, minSize: 200,
size: 800, size: 2000,
maxSize: 800, maxSize: 2000,
enableSorting: true enableSorting: true
}), }),
columnHelper.accessor('time_update', { columnHelper.accessor('time_update', {
@ -61,7 +61,7 @@ function ViewSubscriptions({ items }: ViewSubscriptionsProps) {
animate={{ ...animateSideView.animate }} animate={{ ...animateSideView.animate }}
exit={{ ...animateSideView.exit }} exit={{ ...animateSideView.exit }}
> >
<h1 className='mb-6'>Отслеживаемые схемы</h1> <h1 className='mb-6 select-none'>Отслеживаемые схемы</h1>
<DataTable <DataTable
dense dense
noFooter noFooter

View File

@ -222,11 +222,11 @@ export function labelCstMatchMode(mode: CstMatchMode): string {
export function describeCstMatchMode(mode: CstMatchMode): string { export function describeCstMatchMode(mode: CstMatchMode): string {
// prettier-ignore // prettier-ignore
switch (mode) { switch (mode) {
case CstMatchMode.ALL: return 'искать во всех атрибутах'; case CstMatchMode.ALL: return 'все атрибуты';
case CstMatchMode.EXPR: return 'искать в формальных выражениях'; case CstMatchMode.EXPR: return 'формальное определение';
case CstMatchMode.TERM: return 'искать в терминах'; case CstMatchMode.TERM: return 'термин';
case CstMatchMode.TEXT: return 'искать в определениях и конвенциях'; case CstMatchMode.TEXT: return 'определение и конвенция';
case CstMatchMode.NAME: return 'искать в идентификаторах конституент'; case CstMatchMode.NAME: return 'только имена';
} }
} }
@ -269,7 +269,6 @@ export function labelLibraryFilter(strategy: LibraryFilterStrategy): string {
case LibraryFilterStrategy.MANUAL: return 'отображать все'; case LibraryFilterStrategy.MANUAL: return 'отображать все';
case LibraryFilterStrategy.COMMON: return 'общедоступные'; case LibraryFilterStrategy.COMMON: return 'общедоступные';
case LibraryFilterStrategy.CANONICAL: return 'неизменные'; case LibraryFilterStrategy.CANONICAL: return 'неизменные';
case LibraryFilterStrategy.PERSONAL: return 'личные';
case LibraryFilterStrategy.SUBSCRIBE: return 'подписки'; case LibraryFilterStrategy.SUBSCRIBE: return 'подписки';
case LibraryFilterStrategy.OWNED: return 'владелец'; case LibraryFilterStrategy.OWNED: return 'владелец';
} }
@ -284,7 +283,6 @@ export function describeLibraryFilter(strategy: LibraryFilterStrategy): string {
case LibraryFilterStrategy.MANUAL: return 'Отображать все схемы'; case LibraryFilterStrategy.MANUAL: return 'Отображать все схемы';
case LibraryFilterStrategy.COMMON: return 'Отображать общедоступные схемы'; case LibraryFilterStrategy.COMMON: return 'Отображать общедоступные схемы';
case LibraryFilterStrategy.CANONICAL: return 'Отображать стандартные схемы'; case LibraryFilterStrategy.CANONICAL: return 'Отображать стандартные схемы';
case LibraryFilterStrategy.PERSONAL: return 'Отображать подписки и собственные схемы';
case LibraryFilterStrategy.SUBSCRIBE: return 'Отображать подписки'; case LibraryFilterStrategy.SUBSCRIBE: return 'Отображать подписки';
case LibraryFilterStrategy.OWNED: return 'Отображать собственные схемы'; case LibraryFilterStrategy.OWNED: return 'Отображать собственные схемы';
} }