npm update and linter fixes
This commit is contained in:
parent
b55f33c17d
commit
22eb2a482c
969
rsconcept/frontend/package-lock.json
generated
969
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -10,7 +10,7 @@
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
|
"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"
|
"preview": "vite preview --port 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -30,57 +30,57 @@
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.545.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.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-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.11",
|
"react-intl": "^7.1.14",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.4",
|
||||||
"react-scan": "^0.4.3",
|
"react-scan": "^0.4.3",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.29.1",
|
"react-tooltip": "^5.30.0",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
"zod": "^4.1.11",
|
"zod": "^4.1.12",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@playwright/test": "^1.55.1",
|
"@playwright/test": "^1.56.0",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.5.2",
|
"@types/node": "^24.7.2",
|
||||||
"@types/react": "^19.1.15",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-playwright": "^2.2.2",
|
"eslint-plugin-playwright": "^2.2.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"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",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"stylelint": "^16.24.0",
|
"stylelint": "^16.25.0",
|
||||||
"stylelint-config-recommended": "^16.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.4.4",
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.45.0",
|
"typescript-eslint": "^8.46.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.9"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
||||||
|
|
@ -14,34 +14,40 @@ import { useConceptNavigation } from './navigation-context';
|
||||||
|
|
||||||
export function MenuAI() {
|
export function MenuAI() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
|
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
|
||||||
|
|
||||||
function navigateTemplates(event: React.MouseEvent<Element>) {
|
function navigateTemplates(event: React.MouseEvent<Element>) {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
|
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreatePrompt(event: React.MouseEvent<Element>) {
|
function handleCreatePrompt(event: React.MouseEvent<Element>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
menu.hide();
|
hideMenu();
|
||||||
showAIPrompt();
|
showAIPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<NavigationButton
|
||||||
title='ИИ помощник' //
|
title='ИИ помощник' //
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
aria-expanded={menu.isOpen}
|
aria-expanded={isMenuOpen}
|
||||||
aria-controls={globalIDs.ai_dropdown}
|
aria-controls={globalIDs.ai_dropdown}
|
||||||
icon={<IconAssistant size='1.5rem' />}
|
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
|
<DropdownButton
|
||||||
text='Запрос'
|
text='Запрос'
|
||||||
title='Создать запрос'
|
title='Создать запрос'
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,23 @@ import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export function MenuUser() {
|
export function MenuUser() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
return (
|
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} />}>
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
<UserButton
|
<UserButton
|
||||||
onLogin={() => router.push({ path: urls.login, force: true })}
|
onLogin={() => router.push({ path: urls.login, force: true })}
|
||||||
onClickUser={menu.toggle}
|
onClickUser={toggleMenu}
|
||||||
isOpen={menu.isOpen}
|
isOpen={isMenuOpen}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
<UserDropdown isOpen={isMenuOpen} hideDropdown={() => hideMenu()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function ExportDropdown<T extends object = object>({
|
||||||
filename = 'export',
|
filename = 'export',
|
||||||
className
|
className
|
||||||
}: ExportDropdownProps<T>) {
|
}: ExportDropdownProps<T>) {
|
||||||
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||||
|
|
||||||
function handleExport(format: 'csv' | 'json') {
|
function handleExport(format: 'csv' | 'json') {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
'use no memo';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { useRef, useState } from 'react';
|
||||||
|
|
||||||
export function useDropdown() {
|
export function useDropdown() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
|
||||||
const nextTarget = event.relatedTarget as Node | null;
|
const nextTarget = event.relatedTarget as Node | null;
|
||||||
if (nextTarget && ref.current?.contains(nextTarget)) {
|
if (nextTarget && elementRef.current?.contains(nextTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function useDropdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref,
|
elementRef,
|
||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
handleBlur,
|
handleBlur,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
@ -63,24 +63,25 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
|
||||||
|
|
||||||
const prevReset = useRef(toggleReset);
|
const prevReset = useRef(toggleReset);
|
||||||
const prevTemplate = useRef(promptTemplate);
|
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);
|
useEffect(() => {
|
||||||
if (prevDirty.current !== isDirty) {
|
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
|
||||||
prevDirty.current = isDirty;
|
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);
|
setIsModified(isDirty);
|
||||||
}
|
}, [isDirty, setIsModified]);
|
||||||
|
|
||||||
function onSubmit(data: IUpdatePromptTemplateDTO) {
|
function onSubmit(data: IUpdatePromptTemplateDTO) {
|
||||||
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,19 @@ interface TopicsDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
||||||
const menu = useDropdown();
|
const { elementRef, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||||
const treeHeight = useFitHeight('4rem + 2px');
|
const treeHeight = useFitHeight('4rem + 2px');
|
||||||
|
|
||||||
function handleSelectTopic(topic: HelpTopic) {
|
function handleSelectTopic(topic: HelpTopic) {
|
||||||
menu.hide();
|
hide();
|
||||||
onChangeTopic(topic);
|
onChangeTopic(topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menu.ref}
|
ref={elementRef}
|
||||||
onBlur={menu.handleBlur}
|
onBlur={handleBlur}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute left-0 w-54', //
|
'absolute left-0 w-54', //
|
||||||
noNavigation ? 'top-0' : 'top-12',
|
noNavigation ? 'top-0' : 'top-12',
|
||||||
|
|
@ -43,10 +43,10 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
<Button
|
<Button
|
||||||
noOutline
|
noOutline
|
||||||
title='Список тем'
|
title='Список тем'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isOpen}
|
||||||
icon={!menu.isOpen ? <IconMenuUnfold size='1.25rem' /> : <IconMenuFold size='1.25rem' />}
|
icon={!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')}
|
className={clsx('w-12 h-7 rounded-none border-l-0', isOpen && 'border-b-0')}
|
||||||
onClick={menu.toggle}
|
onClick={toggle}
|
||||||
/>
|
/>
|
||||||
<SelectTree
|
<SelectTree
|
||||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
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}
|
getParent={item => topicParent.get(item) ?? item}
|
||||||
getLabel={labelHelpTopic}
|
getLabel={labelHelpTopic}
|
||||||
getDescription={describeHelpTopic}
|
getDescription={describeHelpTopic}
|
||||||
className={clsx(
|
className={clsx('cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary', isOpen && 'open')}
|
||||||
'cc-topic-dropdown border-r border-t rounded-none cc-scroll-y bg-secondary',
|
|
||||||
menu.isOpen && 'open'
|
|
||||||
)}
|
|
||||||
style={{ maxHeight: treeHeight }}
|
style={{ maxHeight: treeHeight }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,15 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
||||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
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) {
|
const onSelectUser = function (newValue: number) {
|
||||||
ownerSelector.hide();
|
hideOwner();
|
||||||
if (newValue === schema.owner) {
|
if (newValue === schema.owner) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -103,12 +109,12 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='relative' ref={ownerSelector.ref} onBlur={ownerSelector.handleBlur}>
|
<div className='relative' ref={ownerRef} onBlur={handleOwnerBlur}>
|
||||||
<SelectUser
|
<SelectUser
|
||||||
className='absolute -top-2 right-0 w-100 text-sm'
|
className='absolute -top-2 right-0 w-100 text-sm'
|
||||||
value={schema.owner}
|
value={schema.owner}
|
||||||
onChange={user => user && onSelectUser(user)}
|
onChange={user => user && onSelectUser(user)}
|
||||||
hidden={!ownerSelector.isOpen}
|
hidden={!isOwnerOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
|
|
@ -116,7 +122,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
icon={<IconOwner size='1.25rem' className='icon-primary' />}
|
icon={<IconOwner size='1.25rem' className='icon-primary' />}
|
||||||
value={getUserLabel(schema.owner)}
|
value={getUserLabel(schema.owner)}
|
||||||
title={isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
|
title={isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
|
||||||
onClick={ownerSelector.toggle}
|
onClick={toggleOwner}
|
||||||
disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || isAttachedToOSS || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,19 @@ interface MenuRoleProps {
|
||||||
export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
const { user, isAnonymous } = useAuthSuspense();
|
||||||
const router = useConceptNavigation();
|
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 role = useRoleStore(state => state.role);
|
||||||
const setRole = useRoleStore(state => state.setRole);
|
const setRole = useRoleStore(state => state.setRole);
|
||||||
|
|
||||||
function handleChangeMode(newMode: UserRole) {
|
function handleChangeMode(newMode: UserRole) {
|
||||||
accessMenu.hide();
|
hideAccess();
|
||||||
setRole(newMode);
|
setRole(newMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +40,7 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
noPadding
|
||||||
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={isAccessOpen}
|
||||||
className='h-full pr-2 pl-3 bg-transparent'
|
className='h-full pr-2 pl-3 bg-transparent'
|
||||||
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
||||||
onClick={() => router.push({ path: urls.login })}
|
onClick={() => router.push({ path: urls.login })}
|
||||||
|
|
@ -43,17 +49,17 @@ export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={accessMenu.ref} onBlur={accessMenu.handleBlur} className='relative'>
|
<div ref={accessMenuRef} onBlur={handleAccessBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
title={`Режим ${labelUserRole(role)}`}
|
title={`Режим ${labelUserRole(role)}`}
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={isAccessOpen}
|
||||||
className='h-full pr-2 text-muted-foreground hover:text-primary cc-animate-color'
|
className='h-full pr-2 text-muted-foreground hover:text-primary cc-animate-color'
|
||||||
icon={<IconRole value={role} size='1.25rem' className='' />}
|
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
|
<DropdownButton
|
||||||
text={labelUserRole(UserRole.READER)}
|
text={labelUserRole(UserRole.READER)}
|
||||||
title={describeUserRole(UserRole.READER)}
|
title={describeUserRole(UserRole.READER)}
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,26 @@ interface MiniSelectorOSSProps extends Styling {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
|
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>) {
|
function onToggle(event: React.MouseEvent<HTMLElement>) {
|
||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
ossMenu.toggle();
|
toggleOss();
|
||||||
} else {
|
} else {
|
||||||
onSelect(event, items[0]);
|
onSelect(event, items[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ossRef} onBlur={handleOssBlur} className={clsx('relative flex items-center', className)} {...restProps}>
|
||||||
ref={ossMenu.ref}
|
|
||||||
onBlur={ossMenu.handleBlur}
|
|
||||||
className={clsx('relative flex items-center', className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Операционные схемы'
|
title='Операционные схемы'
|
||||||
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
icon={<IconOSS size='1.25rem' className='icon-primary' />}
|
||||||
hideTitle={ossMenu.isOpen}
|
hideTitle={isOssOpen}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
/>
|
/>
|
||||||
{items.length > 1 ? (
|
{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' />
|
<Label text='Список ОСС' className='border-b px-3 py-1' />
|
||||||
{items.map((reference, index) => (
|
{items.map((reference, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
|
|
||||||
|
|
@ -25,29 +25,29 @@ export function SelectLocationContext({
|
||||||
dropdownHeight = 'h-50',
|
dropdownHeight = 'h-50',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectLocationContextProps) {
|
}: SelectLocationContextProps) {
|
||||||
const menu = useDropdown();
|
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
|
||||||
|
|
||||||
function handleClick(event: React.MouseEvent<Element>, newValue: string) {
|
function handleClick(event: React.MouseEvent<Element>, newValue: string) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
menu.hide();
|
hide();
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menu.ref} //
|
ref={elementRef} //
|
||||||
onBlur={menu.handleBlur}
|
onBlur={handleBlur}
|
||||||
className={clsx('text-right self-start select-none', className)}
|
className={clsx('text-right self-start select-none', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={title}
|
title={title}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isOpen}
|
||||||
icon={<IconFolderTree size='1.25rem' className='icon-primary' />}
|
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
|
<SelectLocation
|
||||||
value={value}
|
value={value}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,13 @@ export function PickSchema({
|
||||||
...restProps
|
...restProps
|
||||||
}: PickSchemaProps) {
|
}: PickSchemaProps) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const locationMenu = useDropdown();
|
const {
|
||||||
|
elementRef: locationRef,
|
||||||
|
isOpen: isLocationOpen,
|
||||||
|
toggle: toggleLocation,
|
||||||
|
handleBlur: handleLocationBlur,
|
||||||
|
hide: hideLocationMenu
|
||||||
|
} = useDropdown();
|
||||||
|
|
||||||
const [filterText, setFilterText] = useState(initialFilter);
|
const [filterText, setFilterText] = useState(initialFilter);
|
||||||
const [filterLocation, setFilterLocation] = useState('');
|
const [filterLocation, setFilterLocation] = useState('');
|
||||||
|
|
@ -99,7 +105,7 @@ export function PickSchema({
|
||||||
function handleLocationClick(event: React.MouseEvent<Element>, newValue: string) {
|
function handleLocationClick(event: React.MouseEvent<Element>, newValue: string) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
locationMenu.hide();
|
hideLocationMenu();
|
||||||
setFilterLocation(newValue);
|
setFilterLocation(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,14 +119,14 @@ export function PickSchema({
|
||||||
query={filterText}
|
query={filterText}
|
||||||
onChangeQuery={newValue => setFilterText(newValue)}
|
onChangeQuery={newValue => setFilterText(newValue)}
|
||||||
/>
|
/>
|
||||||
<div className='relative' ref={locationMenu.ref} onBlur={locationMenu.handleBlur}>
|
<div className='relative' ref={locationRef} onBlur={handleLocationBlur}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Фильтр по расположению'
|
title='Фильтр по расположению'
|
||||||
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
icon={<IconFolderTree size='1.25rem' className={!!filterLocation ? 'icon-green' : 'icon-primary'} />}
|
||||||
className='mt-1'
|
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
|
<SelectLocation
|
||||||
value={filterLocation}
|
value={filterLocation}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
|
|
|
||||||
|
|
@ -28,26 +28,26 @@ export function SelectAccessPolicy({
|
||||||
onChange,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectAccessPolicyProps) {
|
}: SelectAccessPolicyProps) {
|
||||||
const menu = useDropdown();
|
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
|
||||||
|
|
||||||
function handleChange(newValue: AccessPolicy) {
|
function handleChange(newValue: AccessPolicy) {
|
||||||
menu.hide();
|
hide();
|
||||||
if (newValue !== value) {
|
if (newValue !== value) {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className={clsx('relative', className)} {...restProps}>
|
<div ref={elementRef} onBlur={handleBlur} className={clsx('relative', className)} {...restProps}>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={`Доступ: ${labelAccessPolicy(value)}`}
|
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isOpen}
|
||||||
className='h-full'
|
className='h-full'
|
||||||
icon={<IconAccessPolicy value={value} size='1.25rem' />}
|
icon={<IconAccessPolicy value={value} size='1.25rem' />}
|
||||||
onClick={menu.toggle}
|
onClick={toggle}
|
||||||
disabled={disabled}
|
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) => (
|
{Object.values(AccessPolicy).map((item, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,27 @@ export function SelectItemType({
|
||||||
onChange,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectItemTypeProps) {
|
}: SelectItemTypeProps) {
|
||||||
const menu = useDropdown();
|
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
|
||||||
|
|
||||||
function handleChange(newValue: LibraryItemType) {
|
function handleChange(newValue: LibraryItemType) {
|
||||||
menu.hide();
|
hide();
|
||||||
if (newValue !== value) {
|
if (newValue !== value) {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
|
<div ref={elementRef} onBlur={handleBlur} className={cn('relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
title={describeLibraryItemType(value)}
|
title={describeLibraryItemType(value)}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isOpen}
|
||||||
className='h-full px-2 py-1 rounded-lg'
|
className='h-full px-2 py-1 rounded-lg'
|
||||||
icon={<IconLibraryItemType value={value} size='1.25rem' />}
|
icon={<IconLibraryItemType value={value} size='1.25rem' />}
|
||||||
text={labelLibraryItemType(value)}
|
text={labelLibraryItemType(value)}
|
||||||
onClick={menu.toggle}
|
onClick={toggle}
|
||||||
disabled={disabled}
|
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) => (
|
{Object.values(LibraryItemType).map((item, index) => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
|
|
|
||||||
|
|
@ -24,31 +24,31 @@ export function SelectLocationHead({
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectLocationHeadProps) {
|
}: SelectLocationHeadProps) {
|
||||||
const menu = useDropdown();
|
const { elementRef, handleBlur, isOpen, toggle, hide } = useDropdown();
|
||||||
|
|
||||||
function handleChange(newValue: LocationHead) {
|
function handleChange(newValue: LocationHead) {
|
||||||
menu.hide();
|
hide();
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menu.ref} //
|
ref={elementRef} //
|
||||||
onBlur={menu.handleBlur}
|
onBlur={handleBlur}
|
||||||
className={cn('text-right relative', className)}
|
className={cn('text-right relative', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
title={describeLocationHead(value)}
|
title={describeLocationHead(value)}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isOpen}
|
||||||
className='h-full'
|
className='h-full'
|
||||||
icon={<IconLocationHead value={value} size='1rem' />}
|
icon={<IconLocationHead value={value} size='1rem' />}
|
||||||
text={labelLocationHead(value)}
|
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)
|
{Object.values(LocationHead)
|
||||||
.filter(head => !excluded.includes(head))
|
.filter(head => !excluded.includes(head))
|
||||||
.map((head, index) => {
|
.map((head, index) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/control';
|
import { MiniButton } from '@/components/control';
|
||||||
|
|
@ -24,25 +24,43 @@ export function SelectLocation({ value, dense, prefix, onClick, className, style
|
||||||
const { folders } = useFolders();
|
const { folders } = useFolders();
|
||||||
const activeNode = folders.at(value);
|
const activeNode = folders.at(value);
|
||||||
const items = folders.getTree();
|
const items = folders.getTree();
|
||||||
const [folded, setFolded] = useState<FolderNode[]>(items);
|
const baseFolded = useMemo(
|
||||||
|
() => items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)),
|
||||||
|
[items, activeNode]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Manual overrides: true => force folded, false => force unfolded
|
||||||
setFolded(items.filter(item => item !== activeNode && !activeNode?.hasPredecessor(item)));
|
const [manualOverrides, setManualOverrides] = useState<Map<FolderNode, boolean>>(new Map());
|
||||||
}, [items, activeNode]);
|
|
||||||
|
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) {
|
function onFoldItem(target: FolderNode, showChildren: boolean) {
|
||||||
setFolded(prev =>
|
setManualOverrides(prev => {
|
||||||
items.filter(item => {
|
const next = new Map(prev);
|
||||||
if (item === target) {
|
if (showChildren) {
|
||||||
return !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;
|
return next;
|
||||||
} else {
|
});
|
||||||
return prev.includes(item);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickFold(event: React.MouseEvent<Element>, target: FolderNode, showChildren: boolean) {
|
function handleClickFold(event: React.MouseEvent<Element>, target: FolderNode, showChildren: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ interface ToolbarSearchProps {
|
||||||
|
|
||||||
export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) {
|
export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps) {
|
||||||
const { items } = useLibrarySuspense();
|
const { items } = useLibrarySuspense();
|
||||||
const userMenu = useDropdown();
|
const {
|
||||||
|
elementRef: userElementRef,
|
||||||
|
handleBlur: userHandleBlur,
|
||||||
|
isOpen: isUserOpen,
|
||||||
|
toggle: toggleUser
|
||||||
|
} = useDropdown();
|
||||||
|
|
||||||
const query = useLibrarySearchStore(state => state.query);
|
const query = useLibrarySearchStore(state => state.query);
|
||||||
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
||||||
|
|
@ -70,14 +75,14 @@ export function ToolbarSearch({ className, total, filtered }: ToolbarSearchProps
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div ref={userMenu.ref} onBlur={userMenu.handleBlur} className='relative flex'>
|
<div ref={userElementRef} onBlur={userHandleBlur} className='relative flex'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Поиск пользователя'
|
title='Поиск пользователя'
|
||||||
hideTitle={userMenu.isOpen}
|
hideTitle={isUserOpen}
|
||||||
icon={<IconUserSearch size='1.25rem' className={userActive ? 'icon-green' : 'icon-primary'} />}
|
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
|
<DropdownButton
|
||||||
text='Я - Владелец'
|
text='Я - Владелец'
|
||||||
title='Фильтровать схемы, в которых текущий пользователь является владельцем'
|
title='Фильтровать схемы, в которых текущий пользователь является владельцем'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
|
@ -48,11 +48,9 @@ export function FormOSS() {
|
||||||
const visible = useWatch({ control, name: 'visible' });
|
const visible = useWatch({ control, name: 'visible' });
|
||||||
const readOnly = useWatch({ control, name: 'read_only' });
|
const readOnly = useWatch({ control, name: 'read_only' });
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
useEffect(() => {
|
||||||
if (prevDirty.current !== isDirty) {
|
|
||||||
prevDirty.current = isDirty;
|
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}
|
}, [isDirty, setIsModified]);
|
||||||
|
|
||||||
function onSubmit(data: IUpdateLibraryItemDTO) {
|
function onSubmit(data: IUpdateLibraryItemDTO) {
|
||||||
return updateOss(data).then(() => reset({ ...data }));
|
return updateOss(data).then(() => reset({ ...data }));
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,13 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
setTimeout(() => fitView(flowOptions.fitViewOptions), PARAMETER.refreshTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
useEffect(() => {
|
||||||
viewportInitialized &&
|
if (!viewportInitialized) return;
|
||||||
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
|
const hasChanged =
|
||||||
) {
|
prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]);
|
||||||
|
|
||||||
|
if (!hasChanged) return;
|
||||||
|
|
||||||
prevSelected.current = selected;
|
prevSelected.current = selected;
|
||||||
setNodes(prev =>
|
setNodes(prev =>
|
||||||
prev.map(node => ({
|
prev.map(node => ({
|
||||||
|
|
@ -115,7 +118,7 @@ export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
selected: selected.includes(node.id)
|
selected: selected.includes(node.id)
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}, [viewportInitialized, selected, setNodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssFlowContext
|
<OssFlowContext
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export function ToolbarSchema({
|
||||||
isMutable,
|
isMutable,
|
||||||
className
|
className
|
||||||
}: ToolbarSchemaProps) {
|
}: ToolbarSchemaProps) {
|
||||||
const menuSchema = useDropdown();
|
const { elementRef, isOpen, handleBlur, toggle, hide } = useDropdown();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
const searchText = useCstSearchStore(state => state.query);
|
const searchText = useCstSearchStore(state => state.query);
|
||||||
|
|
@ -196,25 +196,25 @@ export function ToolbarSchema({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReindex() {
|
function handleReindex() {
|
||||||
menuSchema.hide();
|
hide();
|
||||||
void resetAliases({ itemID: schema.id });
|
void resetAliases({ itemID: schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRestoreOrder() {
|
function handleRestoreOrder() {
|
||||||
menuSchema.hide();
|
hide();
|
||||||
void restoreOrder({ itemID: schema.id });
|
void restoreOrder({ itemID: schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex gap-0.5', className)}>
|
<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
|
<MiniButton
|
||||||
title='Редактирование концептуальной схемы'
|
title='Редактирование концептуальной схемы'
|
||||||
hideTitle={menuSchema.isOpen}
|
hideTitle={isOpen}
|
||||||
icon={<IconRSForm size='1rem' className='icon-primary' />}
|
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
|
<DropdownButton
|
||||||
text='Упорядочить список'
|
text='Упорядочить список'
|
||||||
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ export function ToolbarOssGraph({
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
const { updateLayout } = useUpdateLayout();
|
const { updateLayout } = useUpdateLayout();
|
||||||
const { user } = useAuthSuspense();
|
const { user } = useAuthSuspense();
|
||||||
const menu = useDropdown();
|
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
|
||||||
|
|
||||||
const showOptions = useDialogsStore(state => state.showOssOptions);
|
const showOptions = useDialogsStore(state => state.showOssOptions);
|
||||||
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
|
||||||
|
|
@ -84,7 +84,7 @@ export function ToolbarOssGraph({
|
||||||
|
|
||||||
function handleMenuToggle() {
|
function handleMenuToggle() {
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
menu.toggle();
|
toggleMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShowOptions() {
|
function handleShowOptions() {
|
||||||
|
|
@ -147,7 +147,7 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
aria-label='Сохранить изменения'
|
aria-label='Сохранить изменения'
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', isMac() ? '⌘ + S' : 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', isMac() ? '⌘ + S' : 'Ctrl + S')}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleSavePositions}
|
onClick={handleSavePositions}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
|
|
@ -155,20 +155,20 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
aria-label='Редактировать выбранную'
|
aria-label='Редактировать выбранную'
|
||||||
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
|
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
|
||||||
hideTitle={isContextMenuOpen || menu.isOpen}
|
hideTitle={isContextMenuOpen || isMenuOpen}
|
||||||
icon={<IconEdit size='1.25rem' className='icon-primary' />}
|
icon={<IconEdit size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleEditItem}
|
onClick={handleEditItem}
|
||||||
disabled={selectedItems.length !== 1 || isProcessing}
|
disabled={selectedItems.length !== 1 || isProcessing}
|
||||||
/>
|
/>
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Добавить...'
|
title='Добавить...'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
onClick={handleMenuToggle}
|
onClick={handleMenuToggle}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
|
<Dropdown isOpen={isMenuOpen} className='-translate-x-1/2'>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Новый блок'
|
text='Новый блок'
|
||||||
titleHtml={prepareTooltip('Новый блок', 'Alt + 1')}
|
titleHtml={prepareTooltip('Новый блок', 'Alt + 1')}
|
||||||
|
|
@ -216,7 +216,7 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
aria-label='Удалить выбранную'
|
aria-label='Удалить выбранную'
|
||||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
disabled={
|
disabled={
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,21 @@ export function MenuMain() {
|
||||||
|
|
||||||
const showQR = useDialogsStore(state => state.showQR);
|
const showQR = useDialogsStore(state => state.showQR);
|
||||||
|
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
deleteSchema();
|
deleteSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
sharePage();
|
sharePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,22 +49,22 @@ export function MenuMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShowQR() {
|
function handleShowQR() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
showQR({ target: generatePageQR() });
|
showQR({ target: generatePageQR() });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
title='Меню'
|
title='Меню'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconMenu size='1.25rem' />}
|
icon={<IconMenu size='1.25rem' />}
|
||||||
className='h-full px-2 text-muted-foreground hover:text-primary cc-animate-color'
|
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
|
<DropdownButton
|
||||||
text='Поделиться'
|
text='Поделиться'
|
||||||
title='Скопировать ссылку в буфер обмена'
|
title='Скопировать ссылку в буфер обмена'
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,20 @@ export function ToolbarGraphSelection({
|
||||||
onChange,
|
onChange,
|
||||||
...restProps
|
...restProps
|
||||||
}: ToolbarGraphSelectionProps) {
|
}: ToolbarGraphSelectionProps) {
|
||||||
const selectedMenu = useDropdown();
|
const {
|
||||||
const groupMenu = useDropdown();
|
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;
|
const emptySelection = value.length === 0;
|
||||||
|
|
||||||
function handleSelectReset() {
|
function handleSelectReset() {
|
||||||
|
|
@ -47,23 +59,23 @@ export function ToolbarGraphSelection({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectCore() {
|
function handleSelectCore() {
|
||||||
groupMenu.hide();
|
hideGroup();
|
||||||
const core = [...graph.nodes.keys()].filter(isCore);
|
const core = [...graph.nodes.keys()].filter(isCore);
|
||||||
onChange([...core, ...graph.expandInputs(core)]);
|
onChange([...core, ...graph.expandInputs(core)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectOwned() {
|
function handleSelectOwned() {
|
||||||
groupMenu.hide();
|
hideGroup();
|
||||||
onChange([...graph.nodes.keys()].filter((item: number) => !isInherited(item)));
|
onChange([...graph.nodes.keys()].filter((item: number) => !isInherited(item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectInherited() {
|
function handleSelectInherited() {
|
||||||
groupMenu.hide();
|
hideGroup();
|
||||||
onChange([...graph.nodes.keys()].filter(isInherited));
|
onChange([...graph.nodes.keys()].filter(isInherited));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectCrucial() {
|
function handleSelectCrucial() {
|
||||||
groupMenu.hide();
|
hideGroup();
|
||||||
onChange([...graph.nodes.keys()].filter(isCrucial));
|
onChange([...graph.nodes.keys()].filter(isCrucial));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,22 +88,22 @@ export function ToolbarGraphSelection({
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectMaximize() {
|
function handleSelectMaximize() {
|
||||||
selectedMenu.hide();
|
hideSelected();
|
||||||
onChange(graph.maximizePart(value));
|
onChange(graph.maximizePart(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectInvert() {
|
function handleSelectInvert() {
|
||||||
selectedMenu.hide();
|
hideSelected();
|
||||||
onChange([...graph.nodes.keys()].filter(item => !value.includes(item)));
|
onChange([...graph.nodes.keys()].filter(item => !value.includes(item)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectAllInputs() {
|
function handleSelectAllInputs() {
|
||||||
selectedMenu.hide();
|
hideSelected();
|
||||||
onChange([...value, ...graph.expandAllInputs(value)]);
|
onChange([...value, ...graph.expandAllInputs(value)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectAllOutputs() {
|
function handleSelectAllOutputs() {
|
||||||
selectedMenu.hide();
|
hideSelected();
|
||||||
onChange([...value, ...graph.expandAllOutputs(value)]);
|
onChange([...value, ...graph.expandAllOutputs(value)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,15 +116,15 @@ export function ToolbarGraphSelection({
|
||||||
disabled={emptySelection}
|
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
|
<MiniButton
|
||||||
title='Выделить на основе выбранных...'
|
title='Выделить на основе выбранных...'
|
||||||
hideTitle={selectedMenu.isOpen}
|
hideTitle={isSelectedOpen}
|
||||||
icon={<IconContextSelection size='1.25rem' className='icon-primary' />}
|
icon={<IconContextSelection size='1.25rem' className='icon-primary' />}
|
||||||
onClick={selectedMenu.toggle}
|
onClick={toggleSelected}
|
||||||
disabled={emptySelection}
|
disabled={emptySelection}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={selectedMenu.isOpen} className='-translate-x-1/2'>
|
<Dropdown isOpen={isSelectedOpen} className='-translate-x-1/2'>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Поставщики'
|
text='Поставщики'
|
||||||
title='Выделить поставщиков'
|
title='Выделить поставщиков'
|
||||||
|
|
@ -159,14 +171,14 @@ export function ToolbarGraphSelection({
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={groupMenu.ref} onBlur={groupMenu.handleBlur} className='flex items-center relative'>
|
<div ref={groupElementRef} onBlur={groupHandleBlur} className='flex items-center relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выделить группу...'
|
title='Выделить группу...'
|
||||||
hideTitle={groupMenu.isOpen}
|
hideTitle={isGroupOpen}
|
||||||
icon={<IconGroupSelection size='1.25rem' className='icon-primary' />}
|
icon={<IconGroupSelection size='1.25rem' className='icon-primary' />}
|
||||||
onClick={groupMenu.toggle}
|
onClick={toggleGroup}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={groupMenu.isOpen} stretchLeft>
|
<Dropdown isOpen={isGroupOpen} stretchLeft>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='ядро'
|
text='ядро'
|
||||||
title='Выделить ядро'
|
title='Выделить ядро'
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,31 @@ interface SelectGraphFilterProps extends Styling {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectGraphFilter({ value, dense, className, onChange, ...restProps }: SelectGraphFilterProps) {
|
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) {
|
function handleChange(newValue: DependencyMode) {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
|
<div ref={menuRef} onBlur={handleMenuBlur} className={cn('relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
titleHtml='Настройка фильтрации <br/>по графу термов'
|
titleHtml='Настройка фильтрации <br/>по графу термов'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
className='h-full pr-2'
|
className='h-full pr-2'
|
||||||
icon={<IconDependencyMode value={value} size='1rem' />}
|
icon={<IconDependencyMode value={value} size='1rem' />}
|
||||||
text={!dense ? labelCstSource(value) : undefined}
|
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) => {
|
{Object.values(DependencyMode).map((value, index) => {
|
||||||
const source = value as DependencyMode;
|
const source = value as DependencyMode;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -17,24 +17,30 @@ interface SelectMatchModeProps extends Styling {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectMatchMode({ value, dense, className, onChange, ...restProps }: SelectMatchModeProps) {
|
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) {
|
function handleChange(newValue: CstMatchMode) {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className={cn('relative', className)} {...restProps}>
|
<div ref={menuRef} onBlur={handleMenuBlur} className={cn('relative', className)} {...restProps}>
|
||||||
<SelectorButton
|
<SelectorButton
|
||||||
titleHtml='Настройка фильтрации <br/>по проверяемым атрибутам'
|
titleHtml='Настройка фильтрации <br/>по проверяемым атрибутам'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
className='h-full pr-2'
|
className='h-full pr-2'
|
||||||
icon={<IconCstMatchMode value={value} size='1rem' />}
|
icon={<IconCstMatchMode value={value} size='1rem' />}
|
||||||
text={!dense ? labelCstMatchMode(value) : undefined}
|
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) => {
|
{Object.values(CstMatchMode).map((value, index) => {
|
||||||
const matchMode = value as CstMatchMode;
|
const matchMode = value as CstMatchMode;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
||||||
import { NoData, TextContent } from '@/components/view';
|
import { NoData, TextContent } from '@/components/view';
|
||||||
|
|
@ -37,21 +37,24 @@ export function TableSideConstituents({
|
||||||
const items = useFilteredItems(schema, activeCst);
|
const items = useFilteredItems(schema, activeCst);
|
||||||
|
|
||||||
const prevActiveCstID = useRef<number | null>(null);
|
const prevActiveCstID = useRef<number | null>(null);
|
||||||
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
|
|
||||||
prevActiveCstID.current = activeCst?.id ?? null;
|
useEffect(() => {
|
||||||
if (!!activeCst) {
|
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
|
||||||
setTimeout(() => {
|
prevActiveCstID.current = activeCst?.id ?? null;
|
||||||
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
|
if (!!activeCst) {
|
||||||
if (element) {
|
setTimeout(() => {
|
||||||
element.scrollIntoView({
|
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
|
||||||
behavior: 'smooth',
|
if (element) {
|
||||||
block: 'center',
|
element.scrollIntoView({
|
||||||
inline: 'end'
|
behavior: 'smooth',
|
||||||
});
|
block: 'center',
|
||||||
}
|
inline: 'end'
|
||||||
}, PARAMETER.refreshTimeout);
|
});
|
||||||
|
}
|
||||||
|
}, PARAMETER.refreshTimeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}, [autoScroll, activeCst]);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor('alias', {
|
columnHelper.accessor('alias', {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
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 isElementary = isBaseSet(activeCst.cst_type);
|
||||||
const showConvention = !!activeCst.convention || forceComment || isBasic;
|
const showConvention = !!activeCst.convention || forceComment || isBasic;
|
||||||
|
|
||||||
const prevActiveCstID = useRef(activeCst.id);
|
useLayoutEffect(() => setIsModified(false), [activeCst.id, setIsModified]);
|
||||||
const prevToggleReset = useRef(toggleReset);
|
|
||||||
const prevSchema = useRef(schema);
|
useEffect(() => {
|
||||||
if (
|
|
||||||
prevActiveCstID.current !== activeCst.id ||
|
|
||||||
prevToggleReset.current !== toggleReset ||
|
|
||||||
prevSchema.current !== schema
|
|
||||||
) {
|
|
||||||
prevActiveCstID.current = activeCst.id;
|
|
||||||
prevToggleReset.current = toggleReset;
|
|
||||||
prevSchema.current = schema;
|
|
||||||
reset({
|
reset({
|
||||||
target: activeCst.id,
|
target: activeCst.id,
|
||||||
item_data: {
|
item_data: {
|
||||||
|
|
@ -141,17 +133,30 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
definition_formal: activeCst.definition_formal
|
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);
|
return () => clearTimeout(timeoutId);
|
||||||
if (prevDirty.current !== isDirty) {
|
}, [activeCst.id, toggleReset, schema]);
|
||||||
prevDirty.current = isDirty;
|
|
||||||
|
useEffect(() => {
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}
|
}, [isDirty, setIsModified]);
|
||||||
|
|
||||||
function onSubmit(data: IUpdateConstituentaDTO) {
|
function onSubmit(data: IUpdateConstituentaDTO) {
|
||||||
void updateConstituenta({ itemID: schema.id, data }).then(() => {
|
void updateConstituenta({ itemID: schema.id, data }).then(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
|
@ -57,9 +57,7 @@ export function FormRSForm() {
|
||||||
const visible = useWatch({ control, name: 'visible' });
|
const visible = useWatch({ control, name: 'visible' });
|
||||||
const readOnly = useWatch({ control, name: 'read_only' });
|
const readOnly = useWatch({ control, name: 'read_only' });
|
||||||
|
|
||||||
const prevSchema = useRef(schema);
|
useEffect(() => {
|
||||||
if (prevSchema.current !== schema) {
|
|
||||||
prevSchema.current = schema;
|
|
||||||
reset({
|
reset({
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
item_type: LibraryItemType.RSFORM,
|
item_type: LibraryItemType.RSFORM,
|
||||||
|
|
@ -69,13 +67,11 @@ export function FormRSForm() {
|
||||||
visible: schema.visible,
|
visible: schema.visible,
|
||||||
read_only: schema.read_only
|
read_only: schema.read_only
|
||||||
});
|
});
|
||||||
}
|
}, [schema, reset]);
|
||||||
|
|
||||||
const prevDirty = useRef(isDirty);
|
useEffect(() => {
|
||||||
if (prevDirty.current !== isDirty) {
|
|
||||||
prevDirty.current = isDirty;
|
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}
|
}, [isDirty, setIsModified]);
|
||||||
|
|
||||||
function handleSelectVersion(version: CurrentVersion) {
|
function handleSelectVersion(version: CurrentVersion) {
|
||||||
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ interface ToolbarRSListProps {
|
||||||
export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
const { updateCrucial } = useUpdateCrucial();
|
const { updateCrucial } = useUpdateCrucial();
|
||||||
const menu = useDropdown();
|
const { elementRef: menuRef, isOpen: isMenuOpen, toggle: toggleMenu, handleBlur: handleMenuBlur } = useDropdown();
|
||||||
const {
|
const {
|
||||||
schema,
|
schema,
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -99,15 +99,15 @@ export function ToolbarRSList({ className }: ToolbarRSListProps) {
|
||||||
onClick={handleToggleCrucial}
|
onClick={handleToggleCrucial}
|
||||||
disabled={isProcessing || selected.length === 0}
|
disabled={isProcessing || selected.length === 0}
|
||||||
/>
|
/>
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Добавить пустую конституенту'
|
title='Добавить пустую конституенту'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconOpenList size='1.25rem' className='icon-green' />}
|
icon={<IconOpenList size='1.25rem' className='icon-green' />}
|
||||||
onClick={menu.toggle}
|
onClick={toggleMenu}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
|
<Dropdown isOpen={isMenuOpen} className='-translate-x-1/2'>
|
||||||
{Object.values(CstType).map(typeStr => (
|
{Object.values(CstType).map(typeStr => (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
key={`${prefixes.csttype_list}${typeStr}`}
|
key={`${prefixes.csttype_list}${typeStr}`}
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,14 @@ export function TGFlow() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const prevSelected = useRef<number[]>([]);
|
const prevSelected = useRef<number[]>([]);
|
||||||
if (
|
|
||||||
viewportInitialized &&
|
useEffect(() => {
|
||||||
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
|
if (!viewportInitialized) return;
|
||||||
) {
|
const hasChanged =
|
||||||
|
prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]);
|
||||||
|
|
||||||
|
if (!hasChanged) return;
|
||||||
|
|
||||||
prevSelected.current = selected;
|
prevSelected.current = selected;
|
||||||
setNodes(prev =>
|
setNodes(prev =>
|
||||||
prev.map(node => ({
|
prev.map(node => ({
|
||||||
|
|
@ -139,7 +143,7 @@ export function TGFlow() {
|
||||||
selected: selected.includes(Number(node.id))
|
selected: selected.includes(Number(node.id))
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}, [viewportInitialized, selected, setNodes]);
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,13 @@ export function MenuEditSchema() {
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const isModified = useModificationStore(state => state.isModified);
|
const isModified = useModificationStore(state => state.isModified);
|
||||||
const router = useConceptNavigation();
|
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 { schema, activeCst, setSelected, isArchive, isContentEditable, promptTemplate, deselectAll } = useRSEdit();
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
|
|
||||||
|
|
@ -45,17 +51,17 @@ export function MenuEditSchema() {
|
||||||
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
||||||
|
|
||||||
function handleReindex() {
|
function handleReindex() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
void resetAliases({ itemID: schema.id });
|
void resetAliases({ itemID: schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRestoreOrder() {
|
function handleRestoreOrder() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
void restoreOrder({ itemID: schema.id });
|
void restoreOrder({ itemID: schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubstituteCst() {
|
function handleSubstituteCst() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -66,12 +72,12 @@ export function MenuEditSchema() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTemplates() {
|
function handleTemplates() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
promptTemplate();
|
promptTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProduceStructure(targetCst: IConstituenta | null) {
|
function handleProduceStructure(targetCst: IConstituenta | null) {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
if (!targetCst) {
|
if (!targetCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +95,7 @@ export function MenuEditSchema() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInlineSynthesis() {
|
function handleInlineSynthesis() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +114,7 @@ export function MenuEditSchema() {
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noPadding
|
noPadding
|
||||||
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
|
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
className='h-full px-3 bg-transparent'
|
className='h-full px-3 bg-transparent'
|
||||||
icon={<IconArchive size='1.25rem' className='icon-primary' />}
|
icon={<IconArchive size='1.25rem' className='icon-primary' />}
|
||||||
onClick={event => router.push({ path: urls.schema(schema.id), newTab: event.ctrlKey || event.metaKey })}
|
onClick={event => router.push({ path: urls.schema(schema.id), newTab: event.ctrlKey || event.metaKey })}
|
||||||
|
|
@ -117,17 +123,17 @@ export function MenuEditSchema() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
title='Редактирование'
|
title='Редактирование'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
className='h-full px-3 text-muted-foreground hover:text-primary cc-animate-color'
|
className='h-full px-3 text-muted-foreground hover:text-primary cc-animate-color'
|
||||||
icon={<IconEdit2 size='1.25rem' />}
|
icon={<IconEdit2 size='1.25rem' />}
|
||||||
onClick={menu.toggle}
|
onClick={toggleMenu}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={menu.isOpen} margin='mt-3'>
|
<Dropdown isOpen={isMenuOpen} margin='mt-3'>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Шаблоны'
|
text='Шаблоны'
|
||||||
title='Создать конституенту из шаблона'
|
title='Создать конституенту из шаблона'
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,13 @@ export function MenuMain() {
|
||||||
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
||||||
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
||||||
|
|
||||||
const menu = useDropdown();
|
const {
|
||||||
|
elementRef: menuRef,
|
||||||
|
isOpen: isMenuOpen,
|
||||||
|
toggle: toggleMenu,
|
||||||
|
handleBlur: handleMenuBlur,
|
||||||
|
hide: hideMenu
|
||||||
|
} = useDropdown();
|
||||||
|
|
||||||
function calculateCloneLocation() {
|
function calculateCloneLocation() {
|
||||||
const location = schema.location;
|
const location = schema.location;
|
||||||
|
|
@ -65,12 +71,12 @@ export function MenuMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
deleteSchema();
|
deleteSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownload() {
|
function handleDownload() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -88,12 +94,12 @@ export function MenuMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpload() {
|
function handleUpload() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
showUpload({ itemID: schema.id });
|
showUpload({ itemID: schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClone() {
|
function handleClone() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
if (isModified && !promptUnsaved()) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -106,27 +112,27 @@ export function MenuMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
sharePage();
|
sharePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShowQR() {
|
function handleShowQR() {
|
||||||
menu.hide();
|
hideMenu();
|
||||||
showQR({ target: generatePageQR() });
|
showQR({ target: generatePageQR() });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
|
<div ref={menuRef} onBlur={handleMenuBlur} className='relative'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
noHover
|
noHover
|
||||||
noPadding
|
noPadding
|
||||||
title='Меню'
|
title='Меню'
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={isMenuOpen}
|
||||||
icon={<IconMenu size='1.25rem' />}
|
icon={<IconMenu size='1.25rem' />}
|
||||||
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
|
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
|
<DropdownButton
|
||||||
text='Поделиться'
|
text='Поделиться'
|
||||||
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
|
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { useRoleStore } from './role';
|
import { useRoleStore } from './role';
|
||||||
|
|
||||||
|
|
@ -13,9 +13,11 @@ export function useAdjustRole(input: AdjustRoleProps) {
|
||||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||||
const lastInput = useRef<string | null>(null);
|
const lastInput = useRef<string | null>(null);
|
||||||
|
|
||||||
const serializedInput = JSON.stringify(input);
|
useEffect(() => {
|
||||||
if (lastInput.current !== serializedInput) {
|
const serializedInput = JSON.stringify(input);
|
||||||
lastInput.current = serializedInput;
|
if (lastInput.current !== serializedInput) {
|
||||||
adjustRole(input);
|
lastInput.current = serializedInput;
|
||||||
}
|
adjustRole(input);
|
||||||
|
}
|
||||||
|
}, [input, adjustRole]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { useRef } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
export function useResetOnChange<T>(deps: T[], resetFn: () => void) {
|
export function useResetOnChange<T>(deps: T[], resetFn: () => void) {
|
||||||
const lastDeps = useRef<string | null>(null);
|
const depsKey = useMemo(() => JSON.stringify(deps), [deps]);
|
||||||
const currentDeps = JSON.stringify(deps);
|
useEffect(() => {
|
||||||
if (lastDeps.current !== currentDeps) {
|
|
||||||
lastDeps.current = currentDeps;
|
|
||||||
resetFn();
|
resetFn();
|
||||||
}
|
}, [depsKey, resetFn]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,13 @@ export function useTransitionTracker(delay: number = DEFAULT_DEBOUNCE_DELAY): bo
|
||||||
|
|
||||||
if (navigation.location) {
|
if (navigation.location) {
|
||||||
timeout = setTimeout(() => setShowPending(true), delay);
|
timeout = setTimeout(() => setShowPending(true), delay);
|
||||||
} else {
|
|
||||||
setShowPending(false);
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
setShowPending(false);
|
||||||
};
|
};
|
||||||
}, [navigation.location, delay]);
|
}, [navigation.location, delay]);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user