npm update and linter fixes

This commit is contained in:
Ivan 2025-10-14 01:05:52 +03:00
parent b55f33c17d
commit 22eb2a482c
37 changed files with 896 additions and 765 deletions

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
"dev": "vite --host",
"build": "tsc && vite build",
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 1 --fix",
"preview": "vite preview --port 3000"
},
"dependencies": {
@ -30,57 +30,57 @@
"cmdk": "^1.1.1",
"global": "^4.4.0",
"js-file-download": "^0.4.12",
"lucide-react": "^0.542.0",
"lucide-react": "^0.545.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.63.0",
"react-hook-form": "^7.65.0",
"react-icons": "^5.5.0",
"react-intl": "^7.1.11",
"react-router": "^7.9.3",
"react-intl": "^7.1.14",
"react-router": "^7.9.4",
"react-scan": "^0.4.3",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.29.1",
"react-tooltip": "^5.30.0",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.7",
"use-debounce": "^10.0.6",
"zod": "^4.1.11",
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"@playwright/test": "^1.55.1",
"@tailwindcss/vite": "^4.1.13",
"@playwright/test": "^1.56.0",
"@tailwindcss/vite": "^4.1.14",
"@types/jest": "^30.0.0",
"@types/node": "^24.5.2",
"@types/react": "^19.1.15",
"@types/react-dom": "^19.1.9",
"@types/node": "^24.7.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^5.0.4",
"babel-plugin-react-compiler": "^19.1.0-rc.1",
"eslint": "^9.36.0",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.37.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.4.0",
"jest": "^30.2.0",
"stylelint": "^16.24.0",
"stylelint": "^16.25.0",
"stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7",
"ts-jest": "^29.4.4",
"typescript": "^5.9.2",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
"ts-jest": "^29.4.5",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9"
},
"jest": {
"preset": "ts-jest",

View File

@ -14,34 +14,40 @@ import { useConceptNavigation } from './navigation-context';
export function MenuAI() {
const router = useConceptNavigation();
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
const { user } = useAuth();
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
function navigateTemplates(event: React.MouseEvent<Element>) {
menu.hide();
hideMenu();
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
}
function handleCreatePrompt(event: React.MouseEvent<Element>) {
event.preventDefault();
event.stopPropagation();
menu.hide();
hideMenu();
showAIPrompt();
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
<NavigationButton
title='ИИ помощник' //
hideTitle={menu.isOpen}
aria-expanded={menu.isOpen}
hideTitle={isMenuOpen}
aria-expanded={isMenuOpen}
aria-controls={globalIDs.ai_dropdown}
icon={<IconAssistant size='1.5rem' />}
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={menu.isOpen}>
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={isMenuOpen}>
<DropdownButton
text='Запрос'
title='Создать запрос'

View File

@ -13,17 +13,23 @@ import { UserDropdown } from './user-dropdown';
export function MenuUser() {
const router = useConceptNavigation();
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
<Suspense fallback={<Loader circular scale={1.5} />}>
<UserButton
onLogin={() => router.push({ path: urls.login, force: true })}
onClickUser={menu.toggle}
isOpen={menu.isOpen}
onClickUser={toggleMenu}
isOpen={isMenuOpen}
/>
</Suspense>
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
<UserDropdown isOpen={isMenuOpen} hideDropdown={() => hideMenu()} />
</div>
);
}

View File

@ -33,7 +33,7 @@ export function ExportDropdown<T extends object = object>({
filename = 'export',
className
}: ExportDropdownProps<T>) {
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
function handleExport(format: 'csv' | 'json') {
if (!data || data.length === 0) {

View File

@ -1,4 +1,5 @@
'use client';
'use no memo';
import { useState } from 'react';
import {

View File

@ -4,11 +4,11 @@ import { useRef, useState } from 'react';
export function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const elementRef = useRef<HTMLDivElement>(null);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
const nextTarget = event.relatedTarget as Node | null;
if (nextTarget && ref.current?.contains(nextTarget)) {
if (nextTarget && elementRef.current?.contains(nextTarget)) {
return;
}
@ -23,7 +23,7 @@ export function useDropdown() {
}
return {
ref,
elementRef,
isOpen,
setIsOpen,
handleBlur,

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
@ -63,24 +63,25 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
const prevReset = useRef(toggleReset);
const prevTemplate = useRef(promptTemplate);
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
prevTemplate.current = promptTemplate;
prevReset.current = toggleReset;
reset({
owner: promptTemplate.owner,
label: promptTemplate.label,
description: promptTemplate.description,
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
});
setSampleResult(null);
}
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
useEffect(() => {
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
prevTemplate.current = promptTemplate;
prevReset.current = toggleReset;
reset({
owner: promptTemplate.owner,
label: promptTemplate.label,
description: promptTemplate.description,
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
});
return () => setSampleResult(null);
}
}, [promptTemplate, toggleReset, reset, setSampleResult]);
useEffect(() => {
setIsModified(isDirty);
}
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdatePromptTemplateDTO) {
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {

View File

@ -18,19 +18,19 @@ interface TopicsDropdownProps {
}
export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
const menu = useDropdown();
const { elementRef, isOpen, toggle, handleBlur, hide } = useDropdown();
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const treeHeight = useFitHeight('4rem + 2px');
function handleSelectTopic(topic: HelpTopic) {
menu.hide();
hide();
onChangeTopic(topic);
}
return (
<div
ref={menu.ref}
onBlur={menu.handleBlur}
ref={elementRef}
onBlur={handleBlur}
className={clsx(
'absolute left-0 w-54', //
noNavigation ? 'top-0' : 'top-12',
@ -43,10 +43,10 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
<Button
noOutline
title='Список тем'
hideTitle={menu.isOpen}
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
className={clsx('w-12 h-7 rounded-none border-l-0', menu.isOpen && 'border-b-0')}
onClick={menu.toggle}
hideTitle={isOpen}
icon={!isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
className={clsx('w-12 h-7 rounded-none border-l-0', isOpen && 'border-b-0')}
onClick={toggle}
/>
<SelectTree
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
@ -56,10 +56,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
getParent={item => topicParent.get(item) ?? item}
getLabel={labelHelpTopic}
getDescription={describeHelpTopic}
className={clsx(
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary',
menu.isOpen && 'open'
)}
className={clsx('cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary', isOpen && 'open')}
style={{ maxHeight: treeHeight }}
/>
</div>

View File

@ -53,9 +53,15 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
const showEditEditors = useDialogsStore(state => state.showEditEditors);
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
const ownerSelector = useDropdown();
const {
elementRef: ownerRef,
isOpen: isOwnerOpen,
toggle: toggleOwner,
handleBlur: handleOwnerBlur,
hide: hideOwner
} = useDropdown();
const onSelectUser = function (newValue: number) {
ownerSelector.hide();
hideOwner();
if (newValue === schema.owner) {
return;
}
@ -103,12 +109,12 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
/>
</div>
<div className='relative' ref={ownerSelector.ref} onBlur={ownerSelector.handleBlur}>
<div className='relative' ref={ownerRef} onBlur={handleOwnerBlur}>
<SelectUser
className='absolute -top-2 right-0 w-100 text-sm'
value={schema.owner}
onChange={user => user && onSelectUser(user)}
hidden={!ownerSelector.isOpen}
hidden={!isOwnerOpen}
/>
<ValueIcon
@ -116,7 +122,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
icon={<IconOwner size='1.25rem' className='icon-primary' />}
value={getUserLabel(schema.owner)}
title={isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
onClick={ownerSelector.toggle}
onClick={toggleOwner}
disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
/>
</div>

View File

@ -19,13 +19,19 @@ interface MenuRoleProps {
export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
const { user, isAnonymous } = useAuthSuspense();
const router = useConceptNavigation();
const accessMenu = useDropdown();
const {
elementRef: accessMenuRef,
isOpen: isAccessOpen,
toggle: toggleAccess,
handleBlur: handleAccessBlur,
hide: hideAccess
} = useDropdown();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
function handleChangeMode(newMode: UserRole) {
accessMenu.hide();
hideAccess();
setRole(newMode);
}
@ -34,7 +40,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
<MiniButton
noPadding
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
hideTitle={accessMenu.isOpen}
hideTitle={isAccessOpen}
className='h-full pr-2 pl-3 bg-transparent'
icon={<IconAlert size='1.25rem' className='icon-red' />}
onClick={() => router.push({ path: urls.login })}
@ -43,17 +49,17 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
}
return (
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
<div ref={accessMenuRef} onBlur={handleAccessBlur} className='relative'>
<MiniButton
noHover
noPadding
title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen}
hideTitle={isAccessOpen}
className='h-full pr-2 text-muted-foreground hover:text-primary cc-animate-color'
icon={<IconRole value={role} size='1.25rem' className='' />}
onClick={accessMenu.toggle}
onClick={toggleAccess}
/>
<Dropdown isOpen={accessMenu.isOpen} margin='mt-3'>
<Dropdown isOpen={isAccessOpen} margin='mt-3'>
<DropdownButton
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}

View File

@ -17,31 +17,26 @@ interface MiniSelectorOSSProps extends Styling {
}
export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
const ossMenu = useDropdown();
const { elementRef: ossRef, isOpen: isOssOpen, toggle: toggleOss, handleBlur: handleOssBlur } = useDropdown();
function onToggle(event: React.MouseEvent<HTMLElement>) {
if (items.length > 1) {
ossMenu.toggle();
toggleOss();
} else {
onSelect(event, items[0]);
}
}
return (
<div
ref={ossMenu.ref}
onBlur={ossMenu.handleBlur}
className={clsx('relative flex items-center', className)}
{...restProps}
>
<div ref={ossRef} onBlur={handleOssBlur} className={clsx('relative flex items-center', className)} {...restProps}>
<MiniButton
title='Операционные схемы'
icon={<IconOSS size='1.25rem' className='icon-primary' />}
hideTitle={ossMenu.isOpen}
hideTitle={isOssOpen}
onClick={onToggle}
/>
{items.length > 1 ? (
<Dropdown isOpen={ossMenu.isOpen} margin='mt-1'>
<Dropdown isOpen={isOssOpen} margin='mt-1'>
<Label text='Список ОСС' className='border-b px-3 py-1' />
{items.map((reference, index) => (
<DropdownButton

View File

@ -25,29 +25,29 @@ export function SelectLocationContext({
dropdownHeight = 'h-50',
...restProps
}: SelectLocationContextProps) {
const menu = useDropdown();
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
function handleClick(event: React.MouseEvent<Element>, newValue: string) {
event.preventDefault();
event.stopPropagation();
menu.hide();
hide();
onChange(newValue);
}
return (
<div
ref={menu.ref} //
onBlur={menu.handleBlur}
ref={elementRef} //
onBlur={handleBlur}
className={clsx('text-right self-start select-none', className)}
{...restProps}
>
<MiniButton
title={title}
hideTitle={menu.isOpen}
hideTitle={isOpen}
icon={<IconFolderTree size='1.25rem' className='icon-primary' />}
onClick={() => menu.toggle()}
onClick={toggle}
/>
<Dropdown isOpen={menu.isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
<Dropdown isOpen={isOpen} className={clsx('w-80 z-tooltip', dropdownHeight)}>
<SelectLocation
value={value}
prefix={prefixes.folders_list}

View File

@ -45,7 +45,13 @@ export function PickSchema({
...restProps
}: PickSchemaProps) {
const intl = useIntl();
const locationMenu = useDropdown();
const {
elementRef: locationRef,
isOpen: isLocationOpen,
toggle: toggleLocation,
handleBlur: handleLocationBlur,
hide: hideLocationMenu
} = useDropdown();
const [filterText, setFilterText] = useState(initialFilter);
const [filterLocation, setFilterLocation] = useState('');
@ -99,7 +105,7 @@ export function PickSchema({
function handleLocationClick(event: React.MouseEvent<Element>, newValue: string) {
event.preventDefault();
event.stopPropagation();
locationMenu.hide();
hideLocationMenu();
setFilterLocation(newValue);
}
@ -113,14 +119,14 @@ export function PickSchema({
query={filterText}
onChangeQuery={newValue => setFilterText(newValue)}
/>
<div className='relative' ref={locationMenu.ref} onBlur={locationMenu.handleBlur}>
<div className='relative' ref={locationRef} onBlur={handleLocationBlur}>
<MiniButton
title='Фильтр по расположению'
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
className='mt-1'
onClick={() => locationMenu.toggle()}
onClick={toggleLocation}
/>
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-80 h-50'>
<Dropdown isOpen={isLocationOpen} stretchLeft className='w-80 h-50'>
<SelectLocation
value={filterLocation}
prefix={prefixes.folders_list}

View File

@ -28,26 +28,26 @@ export function SelectAccessPolicy({
onChange,
...restProps
}: SelectAccessPolicyProps) {
const menu = useDropdown();
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
function handleChange(newValue: AccessPolicy) {
menu.hide();
hide();
if (newValue !== value) {
onChange(newValue);
}
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
<div ref={elementRef} onBlur={handleBlur} className={clsx('relative', className)} {...restProps}>
<MiniButton
title={`Доступ: ${labelAccessPolicy(value)}`}
hideTitle={menu.isOpen}
hideTitle={isOpen}
className='h-full'
icon={<IconAccessPolicy value={value} size='1.25rem' />}
onClick={menu.toggle}
onClick={toggle}
disabled={disabled}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
<Dropdown isOpen={isOpen} stretchLeft={stretchLeft} margin='mt-1'>
{Object.values(AccessPolicy).map((item, index) => (
<DropdownButton
key={`${prefixes.policy_list}${index}`}

View File

@ -26,27 +26,27 @@ export function SelectItemType({
onChange,
...restProps
}: SelectItemTypeProps) {
const menu = useDropdown();
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
function handleChange(newValue: LibraryItemType) {
menu.hide();
hide();
if (newValue !== value) {
onChange(newValue);
}
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
<div ref={elementRef} onBlur={handleBlur} className={cn('relative', className)} {...restProps}>
<SelectorButton
title={describeLibraryItemType(value)}
hideTitle={menu.isOpen}
hideTitle={isOpen}
className='h-full px-2 py-1 rounded-lg'
icon={<IconLibraryItemType value={value} size='1.25rem' />}
text={labelLibraryItemType(value)}
onClick={menu.toggle}
onClick={toggle}
disabled={disabled}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft={stretchLeft} margin='mt-1'>
<Dropdown isOpen={isOpen} stretchLeft={stretchLeft} margin='mt-1'>
{Object.values(LibraryItemType).map((item, index) => (
<DropdownButton
key={`${prefixes.policy_list}${index}`}

View File

@ -24,31 +24,31 @@ export function SelectLocationHead({
className,
...restProps
}: SelectLocationHeadProps) {
const menu = useDropdown();
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
function handleChange(newValue: LocationHead) {
menu.hide();
hide();
onChange(newValue);
}
return (
<div
ref={menu.ref} //
onBlur={menu.handleBlur}
ref={elementRef} //
onBlur={handleBlur}
className={cn('text-right relative', className)}
{...restProps}
>
<SelectorButton
tabIndex={-1}
title={describeLocationHead(value)}
hideTitle={menu.isOpen}
hideTitle={isOpen}
className='h-full'
icon={<IconLocationHead value={value} size='1rem' />}
text={labelLocationHead(value)}
onClick={menu.toggle}
onClick={toggle}
/>
<Dropdown isOpen={menu.isOpen} stretchLeft margin='mt-2'>
<Dropdown isOpen={isOpen} stretchLeft margin='mt-2'>
{Object.values(LocationHead)
.filter(head => !excluded.includes(head))
.map((head, index) => {

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useMemo, useState } from 'react';
import clsx from 'clsx';
import { MiniButton } from '@/components/control';
@ -24,25 +24,43 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
const { folders } = useFolders();
const activeNode = folders.at(value);
const items = folders.getTree();
const [folded, setFolded] = useState<FolderNode[]>(items);
const baseFolded = useMemo(
() => items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)),
[items, activeNode]
);
useEffect(() => {
setFolded(items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)));
}, [items, activeNode]);
// Manual overrides: true => force folded, false => force unfolded
const [manualOverrides, setManualOverrides] = useState<Map<FolderNode, boolean>>(new Map());
const folded = useMemo(() => {
const set = new Set<FolderNode>(baseFolded);
manualOverrides.forEach((isFolded, node) => {
if (isFolded) {
set.add(node);
} else {
set.delete(node);
}
});
return Array.from(set);
}, [baseFolded, manualOverrides]);
function onFoldItem(target: FolderNode, showChildren: boolean) {
setFolded(prev =>
items.filter(item => {
if (item === target) {
return !showChildren;
setManualOverrides(prev => {
const next = new Map(prev);
if (showChildren) {
// Currently folded -> unfold target only
next.set(target, false);
} else {
// Currently unfolded -> fold target and all its descendants
next.set(target, true);
for (const item of items) {
if (item !== target && item.hasPredecessor(target)) {
next.set(item, true);
}
}
if (!showChildren && item.hasPredecessor(target)) {
return true;
} else {
return prev.includes(item);
}
})
);
}
return next;
});
}
function handleClickFold(event: React.MouseEvent<Element>, target: FolderNode, showChildren: boolean) {

View File

@ -24,7 +24,12 @@ interface ToolbarSearchProps {
export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) {
const { items } = useLibrarySuspense();
const userMenu = useDropdown();
const {
elementRef: userElementRef,
handleBlur: userHandleBlur,
isOpen: isUserOpen,
toggle: toggleUser
} = useDropdown();
const query = useLibrarySearchStore(state => state.query);
const setQuery = useLibrarySearchStore(state => state.setQuery);
@ -70,14 +75,14 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
onClick={toggleVisible}
/>
<div ref={userMenu.ref} onBlur={userMenu.handleBlur} className='relative flex'>
<div ref={userElementRef} onBlur={userHandleBlur} className='relative flex'>
<MiniButton
title='Поиск пользователя'
hideTitle={userMenu.isOpen}
hideTitle={isUserOpen}
icon={<IconUserSearch size='1.25rem' className={userActive ? 'icon-green' : 'icon-primary'} />}
onClick={userMenu.toggle}
onClick={toggleUser}
/>
<Dropdown isOpen={userMenu.isOpen} margin='mt-1'>
<Dropdown isOpen={isUserOpen} margin='mt-1'>
<DropdownButton
text='Я - Владелец'
title='Фильтровать схемы, в которых текущий пользователь является владельцем'

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client';
import { useRef } from 'react';
import { useEffect } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@ -48,11 +48,9 @@ export function FormOSS() {
const visible = useWatch({ control, name: 'visible' });
const readOnly = useWatch({ control, name: 'read_only' });
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
useEffect(() => {
setIsModified(isDirty);
}
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateLibraryItemDTO) {
return updateOss(data).then(() => reset({ ...data }));

View File

@ -104,10 +104,13 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
}
if (
viewportInitialized &&
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
) {
useEffect(() => {
if (!viewportInitialized) return;
const hasChanged =
prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]);
if (!hasChanged) return;
prevSelected.current = selected;
setNodes(prev =>
prev.map(node => ({
@ -115,7 +118,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
selected: selected.includes(node.id)
}))
);
}
}, [viewportInitialized, selected, setNodes]);
return (
<OssFlowContext

View File

@ -52,7 +52,7 @@ export function ToolbarSchema({
isMutable,
className
}: ToolbarSchemaProps) {
const menuSchema = useDropdown();
const { elementRef, isOpen, handleBlur, toggle, hide } = useDropdown();
const router = useConceptNavigation();
const isProcessing = useMutatingRSForm();
const searchText = useCstSearchStore(state => state.query);
@ -196,25 +196,25 @@ export function ToolbarSchema({
}
function handleReindex() {
menuSchema.hide();
hide();
void resetAliases({ itemID: schema.id });
}
function handleRestoreOrder() {
menuSchema.hide();
hide();
void restoreOrder({ itemID: schema.id });
}
return (
<div className={cn('flex gap-0.5', className)}>
<div ref={menuSchema.ref} onBlur={menuSchema.handleBlur} className='flex relative items-center'>
<div ref={elementRef} onBlur={handleBlur} className='flex relative items-center'>
<MiniButton
title='Редактирование концептуальной схемы'
hideTitle={menuSchema.isOpen}
hideTitle={isOpen}
icon={<IconRSForm size='1rem' className='icon-primary' />}
onClick={menuSchema.toggle}
onClick={toggle}
/>
<Dropdown isOpen={menuSchema.isOpen} margin='mt-0.5'>
<Dropdown isOpen={isOpen} margin='mt-0.5'>
<DropdownButton
text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'

View File

@ -71,7 +71,7 @@ export function ToolbarOssGraph({
const getLayout = useGetLayout();
const { updateLayout } = useUpdateLayout();
const { user } = useAuthSuspense();
const menu = useDropdown();
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
const showOptions = useDialogsStore(state => state.showOssOptions);
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
@ -84,7 +84,7 @@ export function ToolbarOssGraph({
function handleMenuToggle() {
hideContextMenu();
menu.toggle();
toggleMenu();
}
function handleShowOptions() {
@ -147,7 +147,7 @@ export function ToolbarOssGraph({
<MiniButton
aria-label='Сохранить изменения'
titleHtml={prepareTooltip('Сохранить изменения', isMac() ? '⌘ + S' : 'Ctrl + S')}
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={handleSavePositions}
disabled={isProcessing}
@ -155,20 +155,20 @@ export function ToolbarOssGraph({
<MiniButton
aria-label='Редактировать выбранную'
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
hideTitle={isContextMenuOpen || menu.isOpen}
hideTitle={isContextMenuOpen || isMenuOpen}
icon={<IconEdit size='1.25rem' className='icon-primary' />}
onClick={handleEditItem}
disabled={selectedItems.length !== 1 || isProcessing}
/>
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
<MiniButton
title='Добавить...'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconNewItem size='1.25rem' className='icon-green' />}
onClick={handleMenuToggle}
disabled={isProcessing}
/>
<Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
<Dropdown isOpen={isMenuOpen} className='-translate-x-1/2'>
<DropdownButton
text='Новый блок'
titleHtml={prepareTooltip('Новый блок', 'Alt + 1')}
@ -216,7 +216,7 @@ export function ToolbarOssGraph({
<MiniButton
aria-label='Удалить выбранную'
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={onDelete}
disabled={

View File

@ -26,15 +26,21 @@ export function MenuMain() {
const showQR = useDialogsStore(state => state.showQR);
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
function handleDelete() {
menu.hide();
hideMenu();
deleteSchema();
}
function handleShare() {
menu.hide();
hideMenu();
sharePage();
}
@ -43,22 +49,22 @@ export function MenuMain() {
}
function handleShowQR() {
menu.hide();
hideMenu();
showQR({ target: generatePageQR() });
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
<MiniButton
noHover
noPadding
title='Меню'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconMenu size='1.25rem' />}
className='h-full px-2 text-muted-foreground hover:text-primary cc-animate-color'
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown isOpen={menu.isOpen} margin='mt-3'>
<Dropdown isOpen={isMenuOpen} margin='mt-3'>
<DropdownButton
text='Поделиться'
title='Скопировать ссылку в буфер обмена'

View File

@ -38,8 +38,20 @@ export function ToolbarGraphSelection({
onChange,
...restProps
}: ToolbarGraphSelectionProps) {
const selectedMenu = useDropdown();
const groupMenu = useDropdown();
const {
elementRef: selectedElementRef,
handleBlur: selectedHandleBlur,
isOpen: isSelectedOpen,
toggle: toggleSelected,
hide: hideSelected
} = useDropdown();
const {
elementRef: groupElementRef,
handleBlur: groupHandleBlur,
isOpen: isGroupOpen,
toggle: toggleGroup,
hide: hideGroup
} = useDropdown();
const emptySelection = value.length === 0;
function handleSelectReset() {
@ -47,23 +59,23 @@ export function ToolbarGraphSelection({
}
function handleSelectCore() {
groupMenu.hide();
hideGroup();
const core = [...graph.nodes.keys()].filter(isCore);
onChange([...core, ...graph.expandInputs(core)]);
}
function handleSelectOwned() {
groupMenu.hide();
hideGroup();
onChange([...graph.nodes.keys()].filter((item: number) => !isInherited(item)));
}
function handleSelectInherited() {
groupMenu.hide();
hideGroup();
onChange([...graph.nodes.keys()].filter(isInherited));
}
function handleSelectCrucial() {
groupMenu.hide();
hideGroup();
onChange([...graph.nodes.keys()].filter(isCrucial));
}
@ -76,22 +88,22 @@ export function ToolbarGraphSelection({
}
function handleSelectMaximize() {
selectedMenu.hide();
hideSelected();
onChange(graph.maximizePart(value));
}
function handleSelectInvert() {
selectedMenu.hide();
hideSelected();
onChange([...graph.nodes.keys()].filter(item => !value.includes(item)));
}
function handleSelectAllInputs() {
selectedMenu.hide();
hideSelected();
onChange([...value, ...graph.expandAllInputs(value)]);
}
function handleSelectAllOutputs() {
selectedMenu.hide();
hideSelected();
onChange([...value, ...graph.expandAllOutputs(value)]);
}
@ -104,15 +116,15 @@ export function ToolbarGraphSelection({
disabled={emptySelection}
/>
<div ref={selectedMenu.ref} onBlur={selectedMenu.handleBlur} className='flex items-center relative'>
<div ref={selectedElementRef} onBlur={selectedHandleBlur} className='flex items-center relative'>
<MiniButton
title='Выделить на основе выбранных...'
hideTitle={selectedMenu.isOpen}
hideTitle={isSelectedOpen}
icon={<IconContextSelection size='1.25rem' className='icon-primary' />}
onClick={selectedMenu.toggle}
onClick={toggleSelected}
disabled={emptySelection}
/>
<Dropdown isOpen={selectedMenu.isOpen} className='-translate-x-1/2'>
<Dropdown isOpen={isSelectedOpen} className='-translate-x-1/2'>
<DropdownButton
text='Поставщики'
title='Выделить поставщиков'
@ -159,14 +171,14 @@ export function ToolbarGraphSelection({
</Dropdown>
</div>
<div ref={groupMenu.ref} onBlur={groupMenu.handleBlur} className='flex items-center relative'>
<div ref={groupElementRef} onBlur={groupHandleBlur} className='flex items-center relative'>
<MiniButton
title='Выделить группу...'
hideTitle={groupMenu.isOpen}
hideTitle={isGroupOpen}
icon={<IconGroupSelection size='1.25rem' className='icon-primary' />}
onClick={groupMenu.toggle}
onClick={toggleGroup}
/>
<Dropdown isOpen={groupMenu.isOpen} stretchLeft>
<Dropdown isOpen={isGroupOpen} stretchLeft>
<DropdownButton
text='ядро'
title='Выделить ядро'

View File

@ -17,25 +17,31 @@ interface SelectGraphFilterProps extends Styling {
}
export function SelectGraphFilter({ value, dense, className, onChange, ...restProps }: SelectGraphFilterProps) {
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
function handleChange(newValue: DependencyMode) {
menu.hide();
hideMenu();
onChange(newValue);
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
<div ref={menuRef} onBlur={handleMenuBlur} className={cn('relative', className)} {...restProps}>
<SelectorButton
tabIndex={-1}
titleHtml='Настройка фильтрации <br/>по графу термов'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
className='h-full pr-2'
icon={<IconDependencyMode value={value} size='1rem' />}
text={!dense ? labelCstSource(value) : undefined}
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>
<Dropdown stretchLeft isOpen={isMenuOpen} margin='mt-3'>
{Object.values(DependencyMode).map((value, index) => {
const source = value as DependencyMode;
return (

View File

@ -17,24 +17,30 @@ interface SelectMatchModeProps extends Styling {
}
export function SelectMatchMode({ value, dense, className, onChange, ...restProps }: SelectMatchModeProps) {
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
function handleChange(newValue: CstMatchMode) {
menu.hide();
hideMenu();
onChange(newValue);
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
<div ref={menuRef} onBlur={handleMenuBlur} className={cn('relative', className)} {...restProps}>
<SelectorButton
titleHtml='Настройка фильтрации <br/>по проверяемым атрибутам'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
className='h-full pr-2'
icon={<IconCstMatchMode value={value} size='1rem' />}
text={!dense ? labelCstMatchMode(value) : undefined}
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown stretchLeft isOpen={menu.isOpen} margin='mt-3'>
<Dropdown stretchLeft isOpen={isMenuOpen} margin='mt-3'>
{Object.values(CstMatchMode).map((value, index) => {
const matchMode = value as CstMatchMode;
return (

View File

@ -1,6 +1,6 @@
'use client';
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
import { NoData, TextContent } from '@/components/view';
@ -37,21 +37,24 @@ export function TableSideConstituents({
const items = useFilteredItems(schema, activeCst);
const prevActiveCstID = useRef<number | null>(null);
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
prevActiveCstID.current = activeCst?.id ?? null;
if (!!activeCst) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
useEffect(() => {
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
prevActiveCstID.current = activeCst?.id ?? null;
if (!!activeCst) {
setTimeout(() => {
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'end'
});
}
}, PARAMETER.refreshTimeout);
}
}
}
}, [autoScroll, activeCst]);
const columns = [
columnHelper.accessor('alias', {

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import { zodResolver } from '@hookform/resolvers/zod';
@ -121,17 +121,9 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
const isElementary = isBaseSet(activeCst.cst_type);
const showConvention = !!activeCst.convention || forceComment || isBasic;
const prevActiveCstID = useRef(activeCst.id);
const prevToggleReset = useRef(toggleReset);
const prevSchema = useRef(schema);
if (
prevActiveCstID.current !== activeCst.id ||
prevToggleReset.current !== toggleReset ||
prevSchema.current !== schema
) {
prevActiveCstID.current = activeCst.id;
prevToggleReset.current = toggleReset;
prevSchema.current = schema;
useLayoutEffect(() => setIsModified(false), [activeCst.id, setIsModified]);
useEffect(() => {
reset({
target: activeCst.id,
item_data: {
@ -141,17 +133,30 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
definition_formal: activeCst.definition_formal
}
});
setForceComment(false);
setLocalParse(null);
}
}, [
activeCst.id,
activeCst.convention,
activeCst.term_raw,
activeCst.definition_raw,
activeCst.definition_formal,
toggleReset,
schema,
reset
]);
useLayoutEffect(() => setIsModified(false), [activeCst.id, setIsModified]);
useEffect(() => {
// TODO: suspect this is too complex solution
const timeoutId = setTimeout(() => {
setForceComment(false);
setLocalParse(null);
}, 0);
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
return () => clearTimeout(timeoutId);
}, [activeCst.id, toggleReset, schema]);
useEffect(() => {
setIsModified(isDirty);
}
}, [isDirty, setIsModified]);
function onSubmit(data: IUpdateConstituentaDTO) {
void updateConstituenta({ itemID: schema.id, data }).then(() => {

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client';
import { useRef } from 'react';
import { useEffect } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@ -57,9 +57,7 @@ export function FormRSForm() {
const visible = useWatch({ control, name: 'visible' });
const readOnly = useWatch({ control, name: 'read_only' });
const prevSchema = useRef(schema);
if (prevSchema.current !== schema) {
prevSchema.current = schema;
useEffect(() => {
reset({
id: schema.id,
item_type: LibraryItemType.RSFORM,
@ -69,13 +67,11 @@ export function FormRSForm() {
visible: schema.visible,
read_only: schema.read_only
});
}
}, [schema, reset]);
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
useEffect(() => {
setIsModified(isDirty);
}
}, [isDirty, setIsModified]);
function handleSelectVersion(version: CurrentVersion) {
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });

View File

@ -34,7 +34,7 @@ interface ToolbarRSListProps {
export function ToolbarRSList({ className }: ToolbarRSListProps) {
const isProcessing = useMutatingRSForm();
const { updateCrucial } = useUpdateCrucial();
const menu = useDropdown();
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
const {
schema,
selected,
@ -99,15 +99,15 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
onClick={handleToggleCrucial}
disabled={isProcessing || selected.length === 0}
/>
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
<MiniButton
title='Добавить пустую конституенту'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconOpenList size='1.25rem' className='icon-green' />}
onClick={menu.toggle}
onClick={toggleMenu}
disabled={isProcessing}
/>
<Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
<Dropdown isOpen={isMenuOpen} className='-translate-x-1/2'>
{Object.values(CstType).map(typeStr => (
<DropdownButton
key={`${prefixes.csttype_list}${typeStr}`}

View File

@ -128,10 +128,14 @@ export function TGFlow() {
]);
const prevSelected = useRef<number[]>([]);
if (
viewportInitialized &&
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
) {
useEffect(() => {
if (!viewportInitialized) return;
const hasChanged =
prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]);
if (!hasChanged) return;
prevSelected.current = selected;
setNodes(prev =>
prev.map(node => ({
@ -139,7 +143,7 @@ export function TGFlow() {
selected: selected.includes(Number(node.id))
}))
);
}
}, [viewportInitialized, selected, setNodes]);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (isProcessing) {

View File

@ -33,7 +33,13 @@ export function MenuEditSchema() {
const { isAnonymous } = useAuthSuspense();
const isModified = useModificationStore(state => state.isModified);
const router = useConceptNavigation();
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
const { schema, activeCst, setSelected, isArchive, isContentEditable, promptTemplate, deselectAll } = useRSEdit();
const isProcessing = useMutatingRSForm();
@ -45,17 +51,17 @@ export function MenuEditSchema() {
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
function handleReindex() {
menu.hide();
hideMenu();
void resetAliases({ itemID: schema.id });
}
function handleRestoreOrder() {
menu.hide();
hideMenu();
void restoreOrder({ itemID: schema.id });
}
function handleSubstituteCst() {
menu.hide();
hideMenu();
if (isModified && !promptUnsaved()) {
return;
}
@ -66,12 +72,12 @@ export function MenuEditSchema() {
}
function handleTemplates() {
menu.hide();
hideMenu();
promptTemplate();
}
function handleProduceStructure(targetCst: IConstituenta | null) {
menu.hide();
hideMenu();
if (!targetCst) {
return;
}
@ -89,7 +95,7 @@ export function MenuEditSchema() {
}
function handleInlineSynthesis() {
menu.hide();
hideMenu();
if (isModified && !promptUnsaved()) {
return;
}
@ -108,7 +114,7 @@ export function MenuEditSchema() {
<MiniButton
noPadding
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
className='h-full px-3 bg-transparent'
icon={<IconArchive size='1.25rem' className='icon-primary' />}
onClick={event => router.push({ path: urls.schema(schema.id), newTab: event.ctrlKey || event.metaKey })}
@ -117,17 +123,17 @@ export function MenuEditSchema() {
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
<MiniButton
noHover
noPadding
title='Редактирование'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
className='h-full px-3 text-muted-foreground hover:text-primary cc-animate-color'
icon={<IconEdit2 size='1.25rem' />}
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown isOpen={menu.isOpen} margin='mt-3'>
<Dropdown isOpen={isMenuOpen} margin='mt-3'>
<DropdownButton
text='Шаблоны'
title='Создать конституенту из шаблона'

View File

@ -50,7 +50,13 @@ export function MenuMain() {
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const menu = useDropdown();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
function calculateCloneLocation() {
const location = schema.location;
@ -65,12 +71,12 @@ export function MenuMain() {
}
function handleDelete() {
menu.hide();
hideMenu();
deleteSchema();
}
function handleDownload() {
menu.hide();
hideMenu();
if (isModified && !promptUnsaved()) {
return;
}
@ -88,12 +94,12 @@ export function MenuMain() {
}
function handleUpload() {
menu.hide();
hideMenu();
showUpload({ itemID: schema.id });
}
function handleClone() {
menu.hide();
hideMenu();
if (isModified && !promptUnsaved()) {
return;
}
@ -106,27 +112,27 @@ export function MenuMain() {
}
function handleShare() {
menu.hide();
hideMenu();
sharePage();
}
function handleShowQR() {
menu.hide();
hideMenu();
showQR({ target: generatePageQR() });
}
return (
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
<MiniButton
noHover
noPadding
title='Меню'
hideTitle={menu.isOpen}
hideTitle={isMenuOpen}
icon={<IconMenu size='1.25rem' />}
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
onClick={menu.toggle}
onClick={toggleMenu}
/>
<Dropdown isOpen={menu.isOpen} margin='mt-3'>
<Dropdown isOpen={isMenuOpen} margin='mt-3'>
<DropdownButton
text='Поделиться'
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}

View File

@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
import { useRoleStore } from './role';
@ -13,9 +13,11 @@ export function useAdjustRole(input: AdjustRoleProps) {
const adjustRole = useRoleStore(state => state.adjustRole);
const lastInput = useRef<string | null>(null);
const serializedInput = JSON.stringify(input);
if (lastInput.current !== serializedInput) {
lastInput.current = serializedInput;
adjustRole(input);
}
useEffect(() => {
const serializedInput = JSON.stringify(input);
if (lastInput.current !== serializedInput) {
lastInput.current = serializedInput;
adjustRole(input);
}
}, [input, adjustRole]);
}

View File

@ -1,10 +1,8 @@
import { useRef } from 'react';
import { useEffect, useMemo } from 'react';
export function useResetOnChange<T>(deps: T[], resetFn: () => void) {
const lastDeps = useRef<string | null>(null);
const currentDeps = JSON.stringify(deps);
if (lastDeps.current !== currentDeps) {
lastDeps.current = currentDeps;
const depsKey = useMemo(() => JSON.stringify(deps), [deps]);
useEffect(() => {
resetFn();
}
}, [depsKey, resetFn]);
}

View File

@ -19,17 +19,13 @@ export function useTransitionTracker(delay: number = DEFAULT_DEBOUNCE_DELAY): bo
if (navigation.location) {
timeout = setTimeout(() => setShowPending(true), delay);
} else {
setShowPending(false);
if (timeout) {
clearTimeout(timeout);
}
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
setShowPending(false);
};
}, [navigation.location, delay]);