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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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='Создать запрос'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
'use client';
|
||||
'use no memo';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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='Фильтровать схемы, в которых текущий пользователь является владельцем'
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/>логики типов и связей конституент'
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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='Скопировать ссылку в буфер обмена'
|
||||
|
|
|
|||
|
|
@ -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='Выделить ядро'
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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) });
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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='Создать конституенту из шаблона'
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user