Compare commits
25 Commits
6e1d99122e
...
238a22b42f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
238a22b42f | ||
![]() |
8c1cde76fe | ||
![]() |
87e7b36a95 | ||
![]() |
3351ebc637 | ||
![]() |
41aa6106da | ||
![]() |
a470a6c475 | ||
![]() |
efbeb2a343 | ||
![]() |
bade714fbf | ||
![]() |
1911b04094 | ||
![]() |
cab9ae8efc | ||
![]() |
ba11c1f82b | ||
![]() |
be2ea32674 | ||
![]() |
957313dd43 | ||
![]() |
091182cf5f | ||
![]() |
90d35484ad | ||
![]() |
542b137622 | ||
![]() |
63160fe537 | ||
![]() |
dd79312056 | ||
![]() |
75106508e3 | ||
![]() |
1b1b287004 | ||
![]() |
c5238bf1a0 | ||
![]() |
21269a1072 | ||
![]() |
9dcba3c586 | ||
![]() |
b6b57b8b1e | ||
![]() |
d123d96ea3 |
|
@ -45,7 +45,6 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- js-file-download
|
- js-file-download
|
||||||
- use-debounce
|
- use-debounce
|
||||||
- qrcode.react
|
- qrcode.react
|
||||||
- html-to-image
|
|
||||||
- zustand
|
- zustand
|
||||||
- zod
|
- zod
|
||||||
- @hookform/resolvers
|
- @hookform/resolvers
|
||||||
|
|
5
TODO.txt
5
TODO.txt
|
@ -8,9 +8,10 @@ For more specific TODOs see comments in code
|
||||||
- Landing page
|
- Landing page
|
||||||
- Design first user experience
|
- Design first user experience
|
||||||
- Demo sandbox for anonymous users
|
- Demo sandbox for anonymous users
|
||||||
|
- Save react-flow to vector image
|
||||||
|
|
||||||
User profile:
|
User profile:
|
||||||
- Settings + settings server persistency
|
- Settings server persistency
|
||||||
- Profile pictures
|
- Profile pictures
|
||||||
- Custom LibraryItem lists
|
- Custom LibraryItem lists
|
||||||
- Custom user filters and sharing filters
|
- Custom user filters and sharing filters
|
||||||
|
@ -39,7 +40,6 @@ User profile:
|
||||||
|
|
||||||
[Tech]
|
[Tech]
|
||||||
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
|
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
|
||||||
- Testing E2E playwright
|
|
||||||
|
|
||||||
|
|
||||||
[Deployment]
|
[Deployment]
|
||||||
|
@ -60,7 +60,6 @@ Research and consider integration
|
||||||
- skeleton loading
|
- skeleton loading
|
||||||
https://react.dev/reference/react/Suspense
|
https://react.dev/reference/react/Suspense
|
||||||
|
|
||||||
- backend error message unification
|
|
||||||
- drf-messages
|
- drf-messages
|
||||||
https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
|
https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
|
||||||
|
|
||||||
|
|
887
rsconcept/frontend/package-lock.json
generated
887
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -14,17 +14,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hookform/resolvers": "^4.1.0",
|
"@hookform/resolvers": "^4.1.2",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.66.8",
|
"@tanstack/react-query": "^5.66.9",
|
||||||
"@tanstack/react-query-devtools": "^5.66.8",
|
"@tanstack/react-query-devtools": "^5.66.9",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"@uiw/codemirror-themes": "^4.23.8",
|
"@uiw/codemirror-themes": "^4.23.8",
|
||||||
"@uiw/react-codemirror": "^4.23.8",
|
"@uiw/react-codemirror": "^4.23.8",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.8.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"html-to-image": "^1.11.13",
|
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
@ -34,10 +33,10 @@
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.6",
|
"react-intl": "^7.1.6",
|
||||||
"react-router": "^7.2.0",
|
"react-router": "^7.2.0",
|
||||||
"react-scan": "^0.1.3",
|
"react-scan": "^0.1.4",
|
||||||
"react-select": "^5.10.0",
|
"react-select": "^5.10.0",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.3",
|
"react-toastify": "^11.0.5",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
|
@ -48,16 +47,16 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.7.2",
|
"@lezer/generator": "^1.7.2",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@tailwindcss/vite": "^4.0.7",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.13.4",
|
"@types/node": "^22.13.5",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@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": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
||||||
"eslint": "^9.20.1",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
||||||
|
@ -66,10 +65,10 @@
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.6",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.24.1",
|
"typescript-eslint": "^8.25.0",
|
||||||
"vite": "^6.1.1"
|
"vite": "^6.2.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function ApplicationLayout() {
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||||
<ToasterThemed
|
<ToasterThemed
|
||||||
className='text-[14px] cc-animate-position'
|
className='text-[14px]'
|
||||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||||
autoClose={3000}
|
autoClose={3000}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
|
|
@ -1,24 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { Suspense } from 'react';
|
|
||||||
|
|
||||||
import { Tooltip } from '@/components/Container';
|
import { Tooltip } from '@/components/Container';
|
||||||
import { Loader } from '@/components/Loader';
|
|
||||||
import { useTooltipsStore } from '@/stores/tooltips';
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
const InfoConstituenta = React.lazy(() =>
|
|
||||||
import('@/features/rsform/components/InfoConstituenta').then(module => ({ default: module.InfoConstituenta }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const InfoOperation = React.lazy(() =>
|
|
||||||
import('@/features/oss/components/InfoOperation').then(module => ({ default: module.InfoOperation }))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const GlobalTooltips = () => {
|
export const GlobalTooltips = () => {
|
||||||
const hoverCst = useTooltipsStore(state => state.activeCst);
|
|
||||||
const hoverOperation = useTooltipsStore(state => state.activeOperation);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -34,27 +19,6 @@ export const GlobalTooltips = () => {
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
|
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
|
||||||
clickable
|
|
||||||
id={globalIDs.constituenta_tooltip}
|
|
||||||
layer='z-modalTooltip'
|
|
||||||
className='max-w-[30rem]'
|
|
||||||
hidden={!hoverCst}
|
|
||||||
>
|
|
||||||
<Suspense fallback={<Loader />}>
|
|
||||||
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : null}
|
|
||||||
</Suspense>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
id={globalIDs.operation_tooltip}
|
|
||||||
layer='z-modalTooltip'
|
|
||||||
className='max-w-[35rem] max-h-[40rem] dense'
|
|
||||||
hidden={!hoverOperation}
|
|
||||||
>
|
|
||||||
<Suspense fallback={<Loader />}>
|
|
||||||
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
|
|
||||||
</Suspense>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,12 +18,14 @@ export function Navigation() {
|
||||||
const size = useWindowSize();
|
const size = useWindowSize();
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
|
|
||||||
const navigateHome = (event: React.MouseEvent<Element>) => router.push(urls.home, event.ctrlKey || event.metaKey);
|
const navigateHome = (event: React.MouseEvent<Element>) =>
|
||||||
|
router.push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
|
||||||
const navigateLibrary = (event: React.MouseEvent<Element>) =>
|
const navigateLibrary = (event: React.MouseEvent<Element>) =>
|
||||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.library, newTab: event.ctrlKey || event.metaKey });
|
||||||
const navigateHelp = (event: React.MouseEvent<Element>) => router.push(urls.manuals, event.ctrlKey || event.metaKey);
|
const navigateHelp = (event: React.MouseEvent<Element>) =>
|
||||||
|
router.push({ path: urls.manuals, newTab: event.ctrlKey || event.metaKey });
|
||||||
const navigateCreateNew = (event: React.MouseEvent<Element>) =>
|
const navigateCreateNew = (event: React.MouseEvent<Element>) =>
|
||||||
router.push(urls.create_schema, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
|
@ -42,9 +44,10 @@ export function Navigation() {
|
||||||
'cc-shadow-border'
|
'cc-shadow-border'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transitionProperty: 'height, translate',
|
willChange: 'max-height, translate',
|
||||||
|
transitionProperty: 'max-height, translate',
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||||
height: noNavigationAnimation ? '0rem' : '3rem',
|
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
||||||
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,13 +3,19 @@
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
export interface NavigationProps {
|
||||||
|
path: string;
|
||||||
|
newTab?: boolean;
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface INavigationContext {
|
interface INavigationContext {
|
||||||
push: (path: string, newTab?: boolean) => void;
|
push: (props: NavigationProps) => void;
|
||||||
replace: (path: string) => void;
|
pushAsync: (props: NavigationProps) => void | Promise<void>;
|
||||||
back: () => void;
|
replace: (props: Omit<NavigationProps, 'newTab'>) => void;
|
||||||
forward: () => void;
|
replaceAsync: (props: Omit<NavigationProps, 'newTab'>) => void | Promise<void>;
|
||||||
|
back: (force?: boolean) => void;
|
||||||
|
forward: (force?: boolean) => void;
|
||||||
|
|
||||||
canBack: () => boolean;
|
canBack: () => boolean;
|
||||||
|
|
||||||
|
@ -21,7 +27,7 @@ const NavigationContext = createContext<INavigationContext | null>(null);
|
||||||
export const useConceptNavigation = () => {
|
export const useConceptNavigation = () => {
|
||||||
const context = useContext(NavigationContext);
|
const context = useContext(NavigationContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error(contextOutsideScope('useConceptNavigation', 'NavigationState'));
|
throw new Error('useConceptNavigation has to be used within <NavigationState>');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
@ -39,33 +45,47 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
return !!window.history && window.history?.length !== 0;
|
return !!window.history && window.history?.length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function push(path: string, newTab?: boolean) {
|
function push(props: NavigationProps) {
|
||||||
if (newTab) {
|
if (props.newTab) {
|
||||||
window.open(`${path}`, '_blank');
|
window.open(`${props.path}`, '_blank');
|
||||||
return;
|
} else if (props.force || validate()) {
|
||||||
}
|
|
||||||
if (validate()) {
|
|
||||||
Promise.resolve(router(path, { viewTransition: true })).catch(console.error);
|
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replace(path: string) {
|
function pushAsync(props: NavigationProps): void | Promise<void> {
|
||||||
if (validate()) {
|
if (props.newTab) {
|
||||||
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.error);
|
window.open(`${props.path}`, '_blank');
|
||||||
|
} else if (props.force || validate()) {
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
|
return router(props.path, { viewTransition: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
function replace(props: Omit<NavigationProps, 'newTab'>) {
|
||||||
if (validate()) {
|
if (props.force || validate()) {
|
||||||
|
setIsBlocked(false);
|
||||||
|
Promise.resolve(router(props.path, { replace: true, viewTransition: true })).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAsync(props: Omit<NavigationProps, 'newTab'>): void | Promise<void> {
|
||||||
|
if (props.force || validate()) {
|
||||||
|
setIsBlocked(false);
|
||||||
|
return router(props.path, { replace: true, viewTransition: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function back(force?: boolean) {
|
||||||
|
if (force || validate()) {
|
||||||
Promise.resolve(router(-1)).catch(console.error);
|
Promise.resolve(router(-1)).catch(console.error);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forward() {
|
function forward(force?: boolean) {
|
||||||
if (validate()) {
|
if (force || validate()) {
|
||||||
Promise.resolve(router(1)).catch(console.error);
|
Promise.resolve(router(1)).catch(console.error);
|
||||||
setIsBlocked(false);
|
setIsBlocked(false);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +95,9 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
||||||
<NavigationContext
|
<NavigationContext
|
||||||
value={{
|
value={{
|
||||||
push,
|
push,
|
||||||
|
pushAsync,
|
||||||
replace,
|
replace,
|
||||||
|
replaceAsync,
|
||||||
back,
|
back,
|
||||||
forward,
|
forward,
|
||||||
canBack,
|
canBack,
|
||||||
|
|
|
@ -22,7 +22,8 @@ export function ToggleNavigation() {
|
||||||
!noNavigation && 'flex-col-reverse'
|
!noNavigation && 'flex-col-reverse'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transitionProperty: 'height, width, background-color',
|
willChange: 'height, width',
|
||||||
|
transitionProperty: 'height, width',
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||||
height: noNavigationAnimation ? '2rem' : '3rem',
|
height: noNavigationAnimation ? '2rem' : '3rem',
|
||||||
width: noNavigationAnimation ? '3rem' : '2rem'
|
width: noNavigationAnimation ? '3rem' : '2rem'
|
||||||
|
@ -32,7 +33,7 @@ export function ToggleNavigation() {
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type='button'
|
type='button'
|
||||||
className='p-1'
|
className='p-1 cursor-pointer'
|
||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||||
|
@ -44,7 +45,7 @@ export function ToggleNavigation() {
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type='button'
|
type='button'
|
||||||
className='p-1'
|
className='p-1 cursor-pointer'
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
data-tooltip-id={globalIDs.tooltip}
|
||||||
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useAuthSuspense, useLogout } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
import { useLogout } from '@/features/auth/backend/useLogout';
|
||||||
|
|
||||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
||||||
import {
|
import {
|
||||||
|
@ -40,32 +41,32 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
|
|
||||||
function navigateProfile(event: React.MouseEvent<Element>) {
|
function navigateProfile(event: React.MouseEvent<Element>) {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
router.push(urls.profile, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.profile, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function logoutAndRedirect() {
|
function logoutAndRedirect() {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
void logout().then(() => router.push(urls.login));
|
void logout().then(() => router.push({ path: urls.login, force: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoAdmin() {
|
function gotoAdmin() {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
void logout().then(() => router.push(urls.admin, true));
|
void logout().then(() => router.push({ path: urls.admin, force: true, newTab: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoIcons(event: React.MouseEvent<Element>) {
|
function gotoIcons(event: React.MouseEvent<Element>) {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
router.push(urls.icons, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.icons, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoRestApi() {
|
function gotoRestApi() {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
router.push(urls.rest_api, true);
|
router.push({ path: urls.rest_api, newTab: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotoDatabaseSchema(event: React.MouseEvent<Element>) {
|
function gotoDatabaseSchema(event: React.MouseEvent<Element>) {
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
router.push(urls.database_schema, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.database_schema, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleDarkMode() {
|
function handleToggleDarkMode() {
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function UserMenu() {
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
||||||
<Suspense fallback={<Loader circular scale={1.5} />}>
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
<UserButton onLogin={() => router.push(urls.login)} onClickUser={menu.toggle} />
|
<UserButton onLogin={() => router.push({ path: urls.login, force: true })} onClickUser={menu.toggle} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
||||||
import { type z, ZodError } from 'zod';
|
import { type z, ZodError } from 'zod';
|
||||||
|
|
||||||
import { buildConstants } from '@/utils/buildConstants';
|
import { buildConstants } from '@/utils/buildConstants';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
import { extractErrorMessage } from '@/utils/utils';
|
import { extractErrorMessage } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -62,11 +63,7 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
// Note: Ignore cancellation errors
|
// Note: Ignore cancellation errors
|
||||||
if (error.name !== 'CanceledError') {
|
if (error.name !== 'CanceledError') {
|
||||||
if (error instanceof ZodError) {
|
notifyError(error);
|
||||||
toast.error(errorMsg.invalidResponse);
|
|
||||||
} else {
|
|
||||||
toast.error(extractErrorMessage(error));
|
|
||||||
}
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -83,21 +80,11 @@ export function axiosPost<RequestData, ResponseData = void>({
|
||||||
.post<ResponseData>(endpoint, request?.data, options)
|
.post<ResponseData>(endpoint, request?.data, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
if (request?.successMessage) {
|
notifySuccess(response.data, request?.successMessage);
|
||||||
if (typeof request.successMessage === 'string') {
|
|
||||||
toast.success(request.successMessage);
|
|
||||||
} else {
|
|
||||||
toast.success(request.successMessage(response.data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
if (error instanceof ZodError) {
|
notifyError(error);
|
||||||
toast.error(errorMsg.invalidResponse);
|
|
||||||
} else {
|
|
||||||
toast.error(extractErrorMessage(error));
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -112,21 +99,11 @@ export function axiosDelete<RequestData, ResponseData = void>({
|
||||||
.delete<ResponseData>(endpoint, options)
|
.delete<ResponseData>(endpoint, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
if (request?.successMessage) {
|
notifySuccess(response.data, request?.successMessage);
|
||||||
if (typeof request.successMessage === 'string') {
|
|
||||||
toast.success(request.successMessage);
|
|
||||||
} else {
|
|
||||||
toast.success(request.successMessage(response.data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
if (error instanceof ZodError) {
|
notifyError(error);
|
||||||
toast.error(errorMsg.invalidResponse);
|
|
||||||
} else {
|
|
||||||
toast.error(extractErrorMessage(error));
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -141,21 +118,36 @@ export function axiosPatch<RequestData, ResponseData = void>({
|
||||||
.patch<ResponseData>(endpoint, request?.data, options)
|
.patch<ResponseData>(endpoint, request?.data, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
schema?.parse(response.data);
|
schema?.parse(response.data);
|
||||||
if (request?.successMessage) {
|
notifySuccess(response.data, request?.successMessage);
|
||||||
if (typeof request.successMessage === 'string') {
|
|
||||||
toast.success(request.successMessage);
|
|
||||||
} else {
|
|
||||||
toast.success(request.successMessage(response.data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError | ZodError) => {
|
.catch((error: Error | AxiosError | ZodError) => {
|
||||||
if (error instanceof ZodError) {
|
notifyError(error);
|
||||||
toast.error(errorMsg.invalidResponse);
|
|
||||||
} else {
|
|
||||||
toast.error(extractErrorMessage(error));
|
|
||||||
}
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function notifySuccess<ResponseData>(
|
||||||
|
data: ResponseData,
|
||||||
|
message: string | ((data: ResponseData) => string) | undefined
|
||||||
|
) {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
toast.success(message);
|
||||||
|
} else {
|
||||||
|
toast.success(message(data));
|
||||||
|
}
|
||||||
|
}, PARAMETER.notificationDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyError(error: Error | AxiosError | ZodError) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
toast.error(errorMsg.invalidResponse);
|
||||||
|
} else {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
import { LocationHead } from '@/features/library/models/library';
|
|
||||||
import { ExpressionStatus } from '@/features/rsform/models/rsform';
|
|
||||||
import { CstMatchMode, DependencyMode } from '@/features/rsform/stores/cstSearch';
|
|
||||||
|
|
||||||
import {
|
|
||||||
IconAlias,
|
|
||||||
IconBusiness,
|
|
||||||
IconFilter,
|
|
||||||
IconFormula,
|
|
||||||
IconGraphCollapse,
|
|
||||||
IconGraphExpand,
|
|
||||||
IconGraphInputs,
|
|
||||||
IconGraphOutputs,
|
|
||||||
IconHide,
|
|
||||||
IconMoveDown,
|
|
||||||
IconMoveUp,
|
|
||||||
type IconProps,
|
|
||||||
IconPublic,
|
|
||||||
IconSettings,
|
|
||||||
IconShow,
|
|
||||||
IconStatusError,
|
|
||||||
IconStatusIncalculable,
|
|
||||||
IconStatusOK,
|
|
||||||
IconStatusUnknown,
|
|
||||||
IconSubfolders,
|
|
||||||
IconTemplates,
|
|
||||||
IconTerm,
|
|
||||||
IconText,
|
|
||||||
IconUser
|
|
||||||
} from './Icons';
|
|
||||||
|
|
||||||
export interface DomIconProps<RequestData> extends IconProps {
|
|
||||||
value: RequestData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for visibility. */
|
|
||||||
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
|
||||||
if (value) {
|
|
||||||
return <IconShow size={size} className={className ?? 'text-ok-600'} />;
|
|
||||||
} else {
|
|
||||||
return <IconHide size={size} className={className ?? 'text-warn-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for subfolders. */
|
|
||||||
export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
|
||||||
if (value) {
|
|
||||||
return <IconSubfolders size={size} className={className ?? 'text-ok-600'} />;
|
|
||||||
} else {
|
|
||||||
return <IconSubfolders size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for location. */
|
|
||||||
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
|
||||||
switch (value.substring(0, 2) as LocationHead) {
|
|
||||||
case LocationHead.COMMON:
|
|
||||||
return <IconPublic size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case LocationHead.LIBRARY:
|
|
||||||
return <IconTemplates size={size} className={className ?? 'text-warn-600'} />;
|
|
||||||
case LocationHead.PROJECTS:
|
|
||||||
return <IconBusiness size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case LocationHead.USER:
|
|
||||||
return <IconUser size={size} className={className ?? 'text-ok-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for term graph dependency mode. */
|
|
||||||
export function DependencyIcon({ value, size = '1.25rem', className }: DomIconProps<DependencyMode>) {
|
|
||||||
switch (value) {
|
|
||||||
case DependencyMode.ALL:
|
|
||||||
return <IconSettings size={size} className={className} />;
|
|
||||||
case DependencyMode.OUTPUTS:
|
|
||||||
return <IconGraphOutputs size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case DependencyMode.INPUTS:
|
|
||||||
return <IconGraphInputs size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case DependencyMode.EXPAND_OUTPUTS:
|
|
||||||
return <IconGraphExpand size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case DependencyMode.EXPAND_INPUTS:
|
|
||||||
return <IconGraphCollapse size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for constituenta match mode. */
|
|
||||||
export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconProps<CstMatchMode>) {
|
|
||||||
switch (value) {
|
|
||||||
case CstMatchMode.ALL:
|
|
||||||
return <IconFilter size={size} className={className} />;
|
|
||||||
case CstMatchMode.TEXT:
|
|
||||||
return <IconText size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case CstMatchMode.EXPR:
|
|
||||||
return <IconFormula size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case CstMatchMode.TERM:
|
|
||||||
return <IconTerm size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case CstMatchMode.NAME:
|
|
||||||
return <IconAlias size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for expression status. */
|
|
||||||
export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<ExpressionStatus>) {
|
|
||||||
switch (value) {
|
|
||||||
case ExpressionStatus.VERIFIED:
|
|
||||||
case ExpressionStatus.PROPERTY:
|
|
||||||
return <IconStatusOK size={size} className={className} />;
|
|
||||||
|
|
||||||
case ExpressionStatus.UNKNOWN:
|
|
||||||
return <IconStatusUnknown size={size} className={className} />;
|
|
||||||
case ExpressionStatus.INCALCULABLE:
|
|
||||||
return <IconStatusIncalculable size={size} className={className} />;
|
|
||||||
|
|
||||||
case ExpressionStatus.INCORRECT:
|
|
||||||
case ExpressionStatus.UNDEFINED:
|
|
||||||
return <IconStatusError size={size} className={className} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Icon for relocation direction. */
|
|
||||||
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
|
||||||
if (value) {
|
|
||||||
return <IconMoveUp size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
} else {
|
|
||||||
return <IconMoveDown size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -46,6 +46,7 @@ export function Dropdown({
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
willChange: 'clip-path, transform',
|
||||||
transitionProperty: 'clip-path, transform',
|
transitionProperty: 'clip-path, transform',
|
||||||
transitionDuration: `${PARAMETER.dropdownDuration}ms`,
|
transitionDuration: `${PARAMETER.dropdownDuration}ms`,
|
||||||
transitionTimingFunction: 'ease-in-out',
|
transitionTimingFunction: 'ease-in-out',
|
||||||
|
|
|
@ -155,6 +155,10 @@ export { LuCircleDashed as IconAnimation } from 'react-icons/lu';
|
||||||
export { LuCircle as IconAnimationOff } from 'react-icons/lu';
|
export { LuCircle as IconAnimationOff } from 'react-icons/lu';
|
||||||
|
|
||||||
// ===== Custom elements ======
|
// ===== Custom elements ======
|
||||||
|
export interface DomIconProps<RequestData> extends IconProps {
|
||||||
|
value: RequestData;
|
||||||
|
}
|
||||||
|
|
||||||
interface IconSVGProps {
|
interface IconSVGProps {
|
||||||
viewBox: string;
|
viewBox: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
|
|
|
@ -98,11 +98,12 @@ export function SelectTree<ItemType>({
|
||||||
onClick={event => handleSetValue(event, item)}
|
onClick={event => handleSetValue(event, item)}
|
||||||
style={{
|
style={{
|
||||||
borderBottomWidth: isActive ? '1px' : '0px',
|
borderBottomWidth: isActive ? '1px' : '0px',
|
||||||
transitionProperty: 'height, opacity, padding',
|
willChange: 'max-height, opacity, padding',
|
||||||
|
transitionProperty: 'max-height, opacity, padding',
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||||
paddingTop: isActive ? '0.25rem' : '0',
|
paddingTop: isActive ? '0.25rem' : '0',
|
||||||
paddingBottom: isActive ? '0.25rem' : '0',
|
paddingBottom: isActive ? '0.25rem' : '0',
|
||||||
height: isActive ? 'min-content' : '0',
|
maxHeight: isActive ? '1.75rem' : '0',
|
||||||
opacity: isActive ? '1' : '0'
|
opacity: isActive ? '1' : '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { BadgeHelp, type HelpTopic } from '@/features/help';
|
import { type HelpTopic } from '@/features/help';
|
||||||
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { Overlay } from '../Container';
|
||||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
@ -103,13 +105,15 @@ export function ModalForm({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<Overlay className='z-modalOverlay'>
|
||||||
noPadding
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
className='float-right mt-2 mr-2'
|
icon={<IconClose size='1.25rem' />}
|
||||||
onClick={handleCancel}
|
className='float-right mt-2 mr-2'
|
||||||
/>
|
onClick={handleCancel}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
|
|
||||||
|
@ -127,7 +131,7 @@ export function ModalForm({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='z-modalControls my-2 flex gap-12 justify-center text-sm'>
|
<div className='z-modal-controls my-2 flex gap-12 justify-center text-sm'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
autoFocus
|
autoFocus
|
||||||
text={submitText}
|
text={submitText}
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { BadgeHelp } from '@/features/help';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { Overlay } from '../Container';
|
||||||
import { Button, MiniButton } from '../Control';
|
import { Button, MiniButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
|
|
||||||
|
@ -48,13 +49,15 @@ export function ModalView({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<Overlay className='z-modalOverlay'>
|
||||||
noPadding
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
className='float-right mt-2 mr-2'
|
icon={<IconClose size='1.25rem' />}
|
||||||
onClick={hideDialog}
|
className='float-right mt-2 mr-2'
|
||||||
/>
|
onClick={hideDialog}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
|
|
||||||
|
@ -72,7 +75,7 @@ export function ModalView({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='z-modalControls my-2 flex gap-12 justify-center text-sm'>
|
<div className='z-modal-controls my-2 flex gap-12 justify-center text-sm'>
|
||||||
<Button text='Закрыть' className='min-w-[7rem]' onClick={hideDialog} />
|
<Button text='Закрыть' className='min-w-[7rem]' onClick={hideDialog} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { type Styling, type Titled } from '@/components/props';
|
import { type Styling, type Titled } from '@/components/props';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { ValueIcon } from './ValueIcon';
|
import { ValueIcon } from './ValueIcon';
|
||||||
|
|
||||||
|
// characters - threshold for small labels - small font
|
||||||
|
const SMALL_THRESHOLD = 3;
|
||||||
|
|
||||||
interface ValueStatsProps extends Styling, Titled {
|
interface ValueStatsProps extends Styling, Titled {
|
||||||
/** Id of the component. */
|
/** Id of the component. */
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -18,5 +20,5 @@ interface ValueStatsProps extends Styling, Titled {
|
||||||
* Displays statistics value with an icon.
|
* Displays statistics value with an icon.
|
||||||
*/
|
*/
|
||||||
export function ValueStats(props: ValueStatsProps) {
|
export function ValueStats(props: ValueStatsProps) {
|
||||||
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />;
|
return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-[1.4rem]' {...props} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export function ExpectedAnonymous() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
|
|
||||||
function logoutAndRedirect() {
|
function logoutAndRedirect() {
|
||||||
void logout().then(() => router.push(urls.login));
|
void logout().then(() => router.push({ path: urls.login, force: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { ExpectedAnonymous } from './ExpectedAnonymous';
|
||||||
|
export { RequireAuth } from './RequireAuth';
|
|
@ -1,5 +1,2 @@
|
||||||
|
export * from './backend/types';
|
||||||
export { useAuthSuspense } from './backend/useAuth';
|
export { useAuthSuspense } from './backend/useAuth';
|
||||||
export { useChangePassword } from './backend/useChangePassword';
|
|
||||||
export { useLogout } from './backend/useLogout';
|
|
||||||
export { ExpectedAnonymous } from './components/ExpectedAnonymous';
|
|
||||||
export { RequireAuth } from './components/RequireAuth';
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function LoginPage() {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
router.push(urls.library);
|
router.push({ path: urls.library, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,8 @@ export function Component() {
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
token: token
|
token: token
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
router.replace(urls.home);
|
router.replace({ path: urls.home });
|
||||||
router.push(urls.login);
|
router.push({ path: urls.login });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpPro
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} id={`help-${topic}`} className={padding}>
|
<div tabIndex={-1} id={`help-${topic}`} className={padding}>
|
||||||
<IconHelp size='1.25rem' className='icon-primary' />
|
<IconHelp size='1.25rem' className='icon-primary' />
|
||||||
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modalTooltip' {...restProps}>
|
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
|
||||||
<Suspense fallback={<Loader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className='relative' onClick={event => event.stopPropagation()}>
|
<div className='relative' onClick={event => event.stopPropagation()}>
|
||||||
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { CstClass } from '@/features/rsform';
|
||||||
import { colorBgCstClass } from '@/features/rsform/colors';
|
import { colorBgCstClass } from '@/features/rsform/colors';
|
||||||
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
||||||
import { CstClass } from '@/features/rsform/models/rsform';
|
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { ExpressionStatus } from '@/features/rsform';
|
||||||
import { colorBgCstStatus } from '@/features/rsform/colors';
|
import { colorBgCstStatus } from '@/features/rsform/colors';
|
||||||
import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels';
|
import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels';
|
||||||
import { ExpressionStatus } from '@/features/rsform/models/rsform';
|
|
||||||
|
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { BadgeHelp } from './BadgeHelp';
|
|
@ -1,2 +1 @@
|
||||||
export { BadgeHelp } from './components/BadgeHelp';
|
|
||||||
export { HelpTopic } from './models/helpTopic';
|
export { HelpTopic } from './models/helpTopic';
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
IconExecute,
|
IconExecute,
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconImage,
|
|
||||||
IconLineStraight,
|
IconLineStraight,
|
||||||
IconLineWave,
|
IconLineWave,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
|
@ -82,9 +81,6 @@ export function HelpOssGraph() {
|
||||||
<li>
|
<li>
|
||||||
<IconSave className='inline-icon' /> Сохранить положения
|
<IconSave className='inline-icon' /> Сохранить положения
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<IconImage className='inline-icon' /> Сохранить в SVG
|
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
IconGraphInputs,
|
IconGraphInputs,
|
||||||
IconGraphMaximize,
|
IconGraphMaximize,
|
||||||
IconGraphOutputs,
|
IconGraphOutputs,
|
||||||
IconImage,
|
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconOSS,
|
IconOSS,
|
||||||
IconPredecessor,
|
IconPredecessor,
|
||||||
|
@ -85,9 +84,6 @@ export function HelpRSGraphTerm() {
|
||||||
<IconTypeGraph className='inline-icon' /> Открыть{' '}
|
<IconTypeGraph className='inline-icon' /> Открыть{' '}
|
||||||
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
|
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
|
|
||||||
import { HelpTopic } from '../../models/helpTopic';
|
import { HelpTopic } from '../../models/helpTopic';
|
||||||
|
|
||||||
|
@ -19,13 +18,11 @@ export function ManualsPage() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
|
||||||
function onSelectTopic(newTopic: HelpTopic) {
|
function onSelectTopic(newTopic: HelpTopic) {
|
||||||
router.push(urls.help_topic(newTopic));
|
router.push({ path: urls.help_topic(newTopic) });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.values(HelpTopic).includes(activeTopic)) {
|
if (!Object.values(HelpTopic).includes(activeTopic)) {
|
||||||
setTimeout(() => {
|
router.push({ path: urls.page404, force: true });
|
||||||
router.push(urls.page404);
|
|
||||||
}, PARAMETER.refreshTimeout);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
'z-modalTooltip',
|
'z-modal-tooltip',
|
||||||
'text-xs sm:text-sm',
|
'text-xs sm:text-sm',
|
||||||
'select-none',
|
'select-none',
|
||||||
{
|
{
|
||||||
|
@ -66,6 +66,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: treeHeight,
|
maxHeight: treeHeight,
|
||||||
|
willChange: 'clip-path',
|
||||||
transitionProperty: 'clip-path',
|
transitionProperty: 'clip-path',
|
||||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||||
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'
|
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'
|
||||||
|
|
|
@ -6,9 +6,9 @@ export function HomePage() {
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
|
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
router.replace(urls.manuals);
|
router.replace({ path: urls.manuals });
|
||||||
} else {
|
} else {
|
||||||
router.replace(urls.library);
|
router.replace({ path: urls.library });
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
type IVersionCreatedResponse,
|
type IVersionCreatedResponse,
|
||||||
schemaRSForm,
|
schemaRSForm,
|
||||||
schemaVersionCreatedResponse
|
schemaVersionCreatedResponse
|
||||||
} from '@/features/rsform/backend/types';
|
} from '@/features/rsform';
|
||||||
|
|
||||||
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS, KEYS } from '@/backend/configuration';
|
import { DELAYS, KEYS } from '@/backend/configuration';
|
||||||
|
@ -111,9 +111,9 @@ export const libraryApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteItem: (target: number) =>
|
deleteItem: (data: { target: number; beforeInvalidate?: () => void | Promise<void> }) =>
|
||||||
axiosDelete({
|
axiosDelete({
|
||||||
endpoint: `/api/library/${target}`,
|
endpoint: `/api/library/${data.target}`,
|
||||||
request: {
|
request: {
|
||||||
successMessage: infoMsg.itemDestroyed
|
successMessage: infoMsg.itemDestroyed
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,22 +10,23 @@ export const useDeleteItem = () => {
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-item'],
|
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-item'],
|
||||||
mutationFn: libraryApi.deleteItem,
|
mutationFn: libraryApi.deleteItem,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: async (_, variables) => {
|
||||||
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }).catch(console.error);
|
await client.invalidateQueries({ queryKey: libraryApi.libraryListKey });
|
||||||
|
await Promise.resolve(variables.beforeInvalidate?.());
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
void Promise.allSettled([
|
void Promise.allSettled([
|
||||||
client.invalidateQueries({ queryKey: [KEYS.oss] }),
|
client.invalidateQueries({ queryKey: [KEYS.oss] }),
|
||||||
client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables }) }),
|
client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables.target }) }),
|
||||||
client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables }) })
|
client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables.target }) })
|
||||||
]).catch(console.error),
|
]),
|
||||||
PARAMETER.navigationDuration
|
PARAMETER.refreshTimeout
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
deleteItem: (target: number) => mutation.mutateAsync(target),
|
deleteItem: (data: { target: number; beforeInvalidate?: () => void | Promise<void> }) => mutation.mutateAsync(data),
|
||||||
isPending: mutation.isPending
|
isPending: mutation.isPending
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
import { type IRSFormDTO } from '@/features/rsform';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { LocationIcon } from '@/components/DomainIcons';
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { IconLocationHead } from './IconLocationHead';
|
||||||
|
|
||||||
interface BadgeLocationProps {
|
interface BadgeLocationProps {
|
||||||
/** Location to display. */
|
/** Location to display. */
|
||||||
location: string;
|
location: string;
|
||||||
|
@ -12,7 +13,7 @@ interface BadgeLocationProps {
|
||||||
export function BadgeLocation({ location }: BadgeLocationProps) {
|
export function BadgeLocation({ location }: BadgeLocationProps) {
|
||||||
return (
|
return (
|
||||||
<div className='pl-2' data-tooltip-id={globalIDs.tooltip} data-tooltip-content={location}>
|
<div className='pl-2' data-tooltip-id={globalIDs.tooltip} data-tooltip-content={location}>
|
||||||
<LocationIcon value={location} size='1.25rem' />
|
<IconLocationHead value={location} size='1.25rem' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { Suspense } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { InfoUsers, SelectUser, useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
import { useLabelUser, useRoleStore, UserRole } from '@/features/users';
|
||||||
|
import { InfoUsers, SelectUser } from '@/features/users/components';
|
||||||
|
|
||||||
import { Overlay, Tooltip } from '@/components/Container';
|
import { Overlay, Tooltip } from '@/components/Container';
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
|
@ -63,7 +64,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
|
|
||||||
function handleOpenLibrary(event: React.MouseEvent<Element>) {
|
function handleOpenLibrary(event: React.MouseEvent<Element>) {
|
||||||
setGlobalLocation(schema.location);
|
setGlobalLocation(schema.location);
|
||||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
router.push({ path: urls.library, newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditLocation() {
|
function handleEditLocation() {
|
||||||
|
@ -125,7 +126,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
||||||
onClick={handleEditEditors}
|
onClick={handleEditEditors}
|
||||||
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
<Tooltip anchorSelect='#editor_stats' layer='z-modal-tooltip'>
|
||||||
<Suspense fallback={<Loader scale={2} />}>
|
<Suspense fallback={<Loader scale={2} />}>
|
||||||
<InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
|
<InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { type DomIconProps, IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
|
||||||
|
|
||||||
|
import { AccessPolicy } from '../backend/types';
|
||||||
|
|
||||||
|
/** Icon for access policy. */
|
||||||
|
export function IconAccessPolicy({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
||||||
|
switch (value) {
|
||||||
|
case AccessPolicy.PRIVATE:
|
||||||
|
return <IconPrivate size={size} className={className ?? 'text-warn-600'} />;
|
||||||
|
case AccessPolicy.PROTECTED:
|
||||||
|
return <IconProtected size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
case AccessPolicy.PUBLIC:
|
||||||
|
return <IconPublic size={size} className={className ?? 'text-ok-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { type DomIconProps, IconHide, IconShow } from '@/components/Icons';
|
||||||
|
|
||||||
|
/** Icon for visibility. */
|
||||||
|
export function IconItemVisibility({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconShow size={size} className={className ?? 'text-ok-600'} />;
|
||||||
|
} else {
|
||||||
|
return <IconHide size={size} className={className ?? 'text-warn-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { type DomIconProps, IconOSS, IconRSForm } from '@/components/Icons';
|
||||||
|
|
||||||
|
import { LibraryItemType } from '../backend/types';
|
||||||
|
|
||||||
|
/** Icon for library item type. */
|
||||||
|
export function IconLibraryItemType({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
||||||
|
switch (value) {
|
||||||
|
case LibraryItemType.RSFORM:
|
||||||
|
return <IconRSForm size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
case LibraryItemType.OSS:
|
||||||
|
return <IconOSS size={size} className={className ?? 'text-ok-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { type DomIconProps, IconBusiness, IconPublic, IconTemplates, IconUser } from '@/components/Icons';
|
||||||
|
|
||||||
|
import { LocationHead } from '../models/library';
|
||||||
|
|
||||||
|
/** Icon for location. */
|
||||||
|
export function IconLocationHead({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
||||||
|
switch (value.substring(0, 2) as LocationHead) {
|
||||||
|
case LocationHead.COMMON:
|
||||||
|
return <IconPublic size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
case LocationHead.LIBRARY:
|
||||||
|
return <IconTemplates size={size} className={className ?? 'text-warn-600'} />;
|
||||||
|
case LocationHead.PROJECTS:
|
||||||
|
return <IconBusiness size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
case LocationHead.USER:
|
||||||
|
return <IconUser size={size} className={className ?? 'text-ok-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { UserRole } from '@/features/users';
|
||||||
|
|
||||||
|
import { IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/Icons';
|
||||||
|
|
||||||
|
interface IconRoleProps {
|
||||||
|
role: UserRole;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconRole({ role, size = '1.25rem' }: IconRoleProps) {
|
||||||
|
switch (role) {
|
||||||
|
case UserRole.ADMIN:
|
||||||
|
return <IconAdmin size={size} className='icon-primary' />;
|
||||||
|
case UserRole.OWNER:
|
||||||
|
return <IconOwner size={size} className='icon-primary' />;
|
||||||
|
case UserRole.EDITOR:
|
||||||
|
return <IconEditor size={size} className='icon-primary' />;
|
||||||
|
case UserRole.READER:
|
||||||
|
return <IconReader size={size} className='icon-primary' />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { type DomIconProps, IconSubfolders } from '@/components/Icons';
|
||||||
|
|
||||||
|
/** Icon for subfolders. */
|
||||||
|
export function IconShowSubfolders({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconSubfolders size={size} className={className ?? 'text-ok-600'} />;
|
||||||
|
} else {
|
||||||
|
return <IconSubfolders size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
import { describeUserRole, labelUserRole } from '@/features/users/labels';
|
||||||
|
|
||||||
|
import { Button } from '@/components/Control';
|
||||||
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
|
import { IconAlert } from '@/components/Icons';
|
||||||
|
|
||||||
|
import { IconRole } from './IconRole';
|
||||||
|
|
||||||
|
interface MenuRoleProps {
|
||||||
|
isOwned: boolean;
|
||||||
|
isEditor: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
|
||||||
|
const { user, isAnonymous } = useAuthSuspense();
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const accessMenu = useDropdown();
|
||||||
|
|
||||||
|
const role = useRoleStore(state => state.role);
|
||||||
|
const setRole = useRoleStore(state => state.setRole);
|
||||||
|
|
||||||
|
function handleChangeMode(newMode: UserRole) {
|
||||||
|
accessMenu.hide();
|
||||||
|
setRole(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnonymous) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
dense
|
||||||
|
noBorder
|
||||||
|
noOutline
|
||||||
|
tabIndex={-1}
|
||||||
|
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
||||||
|
hideTitle={accessMenu.isOpen}
|
||||||
|
className='h-full pr-2'
|
||||||
|
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
||||||
|
onClick={() => router.push({ path: urls.login })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={accessMenu.ref}>
|
||||||
|
<Button
|
||||||
|
dense
|
||||||
|
noBorder
|
||||||
|
noOutline
|
||||||
|
tabIndex={-1}
|
||||||
|
title={`Режим ${labelUserRole(role)}`}
|
||||||
|
hideTitle={accessMenu.isOpen}
|
||||||
|
className='h-full pr-2'
|
||||||
|
icon={<IconRole role={role} size='1.25rem' />}
|
||||||
|
onClick={accessMenu.toggle}
|
||||||
|
/>
|
||||||
|
<Dropdown isOpen={accessMenu.isOpen}>
|
||||||
|
<DropdownButton
|
||||||
|
text={labelUserRole(UserRole.READER)}
|
||||||
|
title={describeUserRole(UserRole.READER)}
|
||||||
|
icon={<IconRole role={UserRole.READER} size='1rem' />}
|
||||||
|
onClick={() => handleChangeMode(UserRole.READER)}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text={labelUserRole(UserRole.EDITOR)}
|
||||||
|
title={describeUserRole(UserRole.EDITOR)}
|
||||||
|
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
|
||||||
|
disabled={!isOwned && !isEditor}
|
||||||
|
onClick={() => handleChangeMode(UserRole.EDITOR)}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text={labelUserRole(UserRole.OWNER)}
|
||||||
|
title={describeUserRole(UserRole.OWNER)}
|
||||||
|
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
|
||||||
|
disabled={!isOwned}
|
||||||
|
onClick={() => handleChangeMode(UserRole.OWNER)}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text={labelUserRole(UserRole.ADMIN)}
|
||||||
|
title={describeUserRole(UserRole.ADMIN)}
|
||||||
|
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
|
||||||
|
disabled={!user.is_staff}
|
||||||
|
onClick={() => handleChangeMode(UserRole.ADMIN)}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,13 +13,13 @@ import { type ILibraryItemReference } from '../models/library';
|
||||||
|
|
||||||
interface MiniSelectorOSSProps extends Styling {
|
interface MiniSelectorOSSProps extends Styling {
|
||||||
items: ILibraryItemReference[];
|
items: ILibraryItemReference[];
|
||||||
onSelect: (event: React.MouseEvent<Element>, newValue: ILibraryItemReference) => void;
|
onSelect: (event: React.MouseEvent<HTMLElement>, newValue: ILibraryItemReference) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
|
export function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
|
||||||
const ossMenu = useDropdown();
|
const ossMenu = useDropdown();
|
||||||
|
|
||||||
function onToggle(event: React.MouseEvent<Element>) {
|
function onToggle(event: React.MouseEvent<HTMLElement>) {
|
||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
ossMenu.toggle();
|
ossMenu.toggle();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -120,7 +120,7 @@ export function PickSchema({
|
||||||
className='mt-1'
|
className='mt-1'
|
||||||
onClick={() => locationMenu.toggle()}
|
onClick={() => locationMenu.toggle()}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modalTooltip mt-0'>
|
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modal-tooltip mt-0'>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={filterLocation}
|
value={filterLocation}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { type DomIconProps } from '@/components/DomainIcons';
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
|
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { AccessPolicy } from '../backend/types';
|
import { AccessPolicy } from '../backend/types';
|
||||||
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
|
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
|
||||||
|
|
||||||
|
import { IconAccessPolicy } from './IconAccessPolicy';
|
||||||
|
|
||||||
interface SelectAccessPolicyProps extends Styling {
|
interface SelectAccessPolicyProps extends Styling {
|
||||||
value: AccessPolicy;
|
value: AccessPolicy;
|
||||||
onChange: (value: AccessPolicy) => void;
|
onChange: (value: AccessPolicy) => void;
|
||||||
|
@ -34,7 +34,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
||||||
title={`Доступ: ${labelAccessPolicy(value)}`}
|
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
className='h-full'
|
className='h-full'
|
||||||
icon={<PolicyIcon value={value} size='1.25rem' />}
|
icon={<IconAccessPolicy value={value} size='1.25rem' />}
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
@ -44,7 +44,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
text={labelAccessPolicy(item)}
|
text={labelAccessPolicy(item)}
|
||||||
title={describeAccessPolicy(item)}
|
title={describeAccessPolicy(item)}
|
||||||
icon={<PolicyIcon value={item} size='1rem' />}
|
icon={<IconAccessPolicy value={item} size='1rem' />}
|
||||||
onClick={() => handleChange(item)}
|
onClick={() => handleChange(item)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -52,15 +52,3 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Icon for access policy. */
|
|
||||||
function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
|
||||||
switch (value) {
|
|
||||||
case AccessPolicy.PRIVATE:
|
|
||||||
return <IconPrivate size={size} className={className ?? 'text-warn-600'} />;
|
|
||||||
case AccessPolicy.PROTECTED:
|
|
||||||
return <IconProtected size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case AccessPolicy.PUBLIC:
|
|
||||||
return <IconPublic size={size} className={className ?? 'text-ok-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SelectorButton } from '@/components/Control';
|
import { SelectorButton } from '@/components/Control';
|
||||||
import { type DomIconProps } from '@/components/DomainIcons';
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { IconOSS, IconRSForm } from '@/components/Icons';
|
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
import { LibraryItemType } from '../backend/types';
|
import { LibraryItemType } from '../backend/types';
|
||||||
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
|
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
|
||||||
|
|
||||||
|
import { IconLibraryItemType } from './IconLibraryItemType';
|
||||||
|
|
||||||
interface SelectItemTypeProps extends Styling {
|
interface SelectItemTypeProps extends Styling {
|
||||||
value: LibraryItemType;
|
value: LibraryItemType;
|
||||||
onChange: (value: LibraryItemType) => void;
|
onChange: (value: LibraryItemType) => void;
|
||||||
|
@ -34,7 +34,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
||||||
title={describeLibraryItemType(value)}
|
title={describeLibraryItemType(value)}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
className='h-full px-2 py-1 rounded-lg'
|
className='h-full px-2 py-1 rounded-lg'
|
||||||
icon={<ItemTypeIcon value={value} size='1.25rem' />}
|
icon={<IconLibraryItemType value={value} size='1.25rem' />}
|
||||||
text={labelLibraryItemType(value)}
|
text={labelLibraryItemType(value)}
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -45,7 +45,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
||||||
key={`${prefixes.policy_list}${index}`}
|
key={`${prefixes.policy_list}${index}`}
|
||||||
text={labelLibraryItemType(item)}
|
text={labelLibraryItemType(item)}
|
||||||
title={describeLibraryItemType(item)}
|
title={describeLibraryItemType(item)}
|
||||||
icon={<ItemTypeIcon value={item} size='1rem' />}
|
icon={<IconLibraryItemType value={item} size='1rem' />}
|
||||||
onClick={() => handleChange(item)}
|
onClick={() => handleChange(item)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -53,13 +53,3 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Icon for library item type. */
|
|
||||||
function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
|
||||||
switch (value) {
|
|
||||||
case LibraryItemType.RSFORM:
|
|
||||||
return <IconRSForm size={size} className={className ?? 'text-sec-600'} />;
|
|
||||||
case LibraryItemType.OSS:
|
|
||||||
return <IconOSS size={size} className={className ?? 'text-ok-600'} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ export function SelectLocationContext({
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={menu.isOpen}
|
isOpen={menu.isOpen}
|
||||||
className={clsx('w-[20rem] h-[12.5rem] z-modalTooltip mt-[-0.25rem]', className)}
|
className={clsx('w-[20rem] h-[12.5rem] z-modal-tooltip mt-[-0.25rem]', className)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SelectorButton } from '@/components/Control';
|
import { SelectorButton } from '@/components/Control';
|
||||||
import { LocationIcon } from '@/components/DomainIcons';
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
|
@ -11,6 +10,8 @@ import { prefixes } from '@/utils/constants';
|
||||||
import { describeLocationHead, labelLocationHead } from '../labels';
|
import { describeLocationHead, labelLocationHead } from '../labels';
|
||||||
import { LocationHead } from '../models/library';
|
import { LocationHead } from '../models/library';
|
||||||
|
|
||||||
|
import { IconLocationHead } from './IconLocationHead';
|
||||||
|
|
||||||
interface SelectLocationHeadProps extends Styling {
|
interface SelectLocationHeadProps extends Styling {
|
||||||
value: LocationHead;
|
value: LocationHead;
|
||||||
onChange: (newValue: LocationHead) => void;
|
onChange: (newValue: LocationHead) => void;
|
||||||
|
@ -39,12 +40,12 @@ export function SelectLocationHead({
|
||||||
title={describeLocationHead(value)}
|
title={describeLocationHead(value)}
|
||||||
hideTitle={menu.isOpen}
|
hideTitle={menu.isOpen}
|
||||||
className='h-full'
|
className='h-full'
|
||||||
icon={<LocationIcon value={value} size='1rem' />}
|
icon={<IconLocationHead value={value} size='1rem' />}
|
||||||
text={labelLocationHead(value)}
|
text={labelLocationHead(value)}
|
||||||
onClick={menu.toggle}
|
onClick={menu.toggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={menu.isOpen} className='z-modalTooltip'>
|
<Dropdown isOpen={menu.isOpen} className='z-modal-tooltip'>
|
||||||
{Object.values(LocationHead)
|
{Object.values(LocationHead)
|
||||||
.filter(head => !excluded.includes(head))
|
.filter(head => !excluded.includes(head))
|
||||||
.map((head, index) => {
|
.map((head, index) => {
|
||||||
|
@ -56,7 +57,7 @@ export function SelectLocationHead({
|
||||||
title={describeLocationHead(head)}
|
title={describeLocationHead(head)}
|
||||||
>
|
>
|
||||||
<div className='inline-flex items-center gap-3'>
|
<div className='inline-flex items-center gap-3'>
|
||||||
<LocationIcon value={head} size='1rem' />
|
<IconLocationHead value={head} size='1rem' />
|
||||||
{labelLocationHead(head)}
|
{labelLocationHead(head)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { BadgeHelp, HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
import { Overlay } from '@/components/Container';
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
|
||||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||||
import { Label } from '@/components/Input';
|
import { Label } from '@/components/Input';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
@ -12,6 +12,7 @@ import { type AccessPolicy, type ILibraryItem } from '../backend/types';
|
||||||
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||||
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
|
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
|
||||||
|
|
||||||
|
import { IconItemVisibility } from './IconItemVisibility';
|
||||||
import { SelectAccessPolicy } from './SelectAccessPolicy';
|
import { SelectAccessPolicy } from './SelectAccessPolicy';
|
||||||
|
|
||||||
interface ToolbarItemAccessProps {
|
interface ToolbarItemAccessProps {
|
||||||
|
@ -52,7 +53,7 @@ export function ToolbarItemAccess({
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
icon={<VisibilityIcon value={visible} />}
|
icon={<IconItemVisibility value={visible} />}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
disabled={role === UserRole.READER || isProcessing}
|
disabled={role === UserRole.READER || isProcessing}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export { EditorLibraryItem } from './EditorLibraryItem';
|
||||||
|
export { MenuRole } from './MenuRole';
|
||||||
|
export { MiniSelectorOSS } from './MiniSelectorOSS';
|
||||||
|
export { PickSchema } from './PickSchema';
|
||||||
|
export { SelectLibraryItem } from './SelectLibraryItem';
|
||||||
|
export { SelectVersion } from './SelectVersion';
|
||||||
|
export { ToolbarItemAccess } from './ToolbarItemAccess';
|
|
@ -8,13 +8,13 @@ import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
|
||||||
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
|
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
|
||||||
import { ModalForm } from '@/components/Modal';
|
import { ModalForm } from '@/components/Modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
import { AccessPolicy, type ICloneLibraryItemDTO, type ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
|
import { AccessPolicy, type ICloneLibraryItemDTO, type ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
|
||||||
import { useCloneItem } from '../backend/useCloneItem';
|
import { useCloneItem } from '../backend/useCloneItem';
|
||||||
|
import { IconItemVisibility } from '../components/IconItemVisibility';
|
||||||
import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
|
import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
|
||||||
import { SelectLocationContext } from '../components/SelectLocationContext';
|
import { SelectLocationContext } from '../components/SelectLocationContext';
|
||||||
import { SelectLocationHead } from '../components/SelectLocationHead';
|
import { SelectLocationHead } from '../components/SelectLocationHead';
|
||||||
|
@ -60,7 +60,7 @@ export function DlgCloneLibraryItem() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(data: ICloneLibraryItemDTO) {
|
function onSubmit(data: ICloneLibraryItemDTO) {
|
||||||
return cloneItem(data).then(newSchema => router.push(urls.schema(newSchema.id)));
|
return cloneItem(data).then(newSchema => router.pushAsync({ path: urls.schema(newSchema.id), force: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -105,7 +105,7 @@ export function DlgCloneLibraryItem() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
icon={<VisibilityIcon value={field.value} />}
|
icon={<IconItemVisibility value={field.value} />}
|
||||||
onClick={() => field.onChange(!field.value)}
|
onClick={() => field.onChange(!field.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SelectUser, TableUsers, useUsers } from '@/features/users';
|
import { useUsers } from '@/features/users';
|
||||||
|
import { SelectUser, TableUsers } from '@/features/users/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconRemove } from '@/components/Icons';
|
import { IconRemove } from '@/components/Icons';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useMemo } 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';
|
||||||
|
|
||||||
import { useRSFormSuspense } from '@/features/rsform';
|
import { useRSFormSuspense } from '@/features/rsform/backend/useRSForm';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconReset, IconSave } from '@/components/Icons';
|
import { IconReset, IconSave } from '@/components/Icons';
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
export { AccessPolicy, type ILibraryItem, type IVersionInfo, LibraryItemType } from './backend/types';
|
export {
|
||||||
export { useDeleteItem } from './backend/useDeleteItem';
|
AccessPolicy,
|
||||||
export { useLibrary, useLibrarySuspense } from './backend/useLibrary';
|
type ILibraryItem,
|
||||||
export { useMutatingLibrary } from './backend/useMutatingLibrary';
|
type ILibraryItemData,
|
||||||
export { useTemplatesSuspense } from './backend/useTemplates';
|
type IUpdateLibraryItemDTO,
|
||||||
export { useUpdateItem } from './backend/useUpdateItem';
|
type IVersionInfo,
|
||||||
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
|
LibraryItemType,
|
||||||
export { useVersionRestore } from './backend/useVersionRestore';
|
schemaLibraryItem,
|
||||||
export { EditorLibraryItem } from './components/EditorLibraryItem';
|
schemaUpdateLibraryItem,
|
||||||
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
|
schemaVersionInfo
|
||||||
export { PickSchema } from './components/PickSchema';
|
} from './backend/types';
|
||||||
export { SelectLibraryItem } from './components/SelectLibraryItem';
|
export { BASIC_SCHEMAS, type CurrentVersion, type ILibraryItemReference, LocationHead } from './models/library';
|
||||||
export { SelectVersion } from './components/SelectVersion';
|
|
||||||
export { ToolbarItemAccess } from './components/ToolbarItemAccess';
|
|
||||||
export { type ILibraryItemReference } from './models/library';
|
|
||||||
export { useLibrarySearchStore } from './stores/librarySearch';
|
export { useLibrarySearchStore } from './stores/librarySearch';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RequireAuth } from '@/features/auth';
|
import { RequireAuth } from '@/features/auth/components';
|
||||||
|
|
||||||
import { FormCreateItem } from './FormCreateItem';
|
import { FormCreateItem } from './FormCreateItem';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
import { Overlay } from '@/components/Container';
|
||||||
import { Button, MiniButton, SubmitButton } from '@/components/Control';
|
import { Button, MiniButton, SubmitButton } from '@/components/Control';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
|
||||||
import { IconDownload } from '@/components/Icons';
|
import { IconDownload } from '@/components/Icons';
|
||||||
import { InfoError } from '@/components/InfoError';
|
import { InfoError } from '@/components/InfoError';
|
||||||
import { Label, TextArea, TextInput } from '@/components/Input';
|
import { Label, TextArea, TextInput } from '@/components/Input';
|
||||||
|
@ -23,6 +22,7 @@ import {
|
||||||
schemaCreateLibraryItem
|
schemaCreateLibraryItem
|
||||||
} from '../../backend/types';
|
} from '../../backend/types';
|
||||||
import { useCreateItem } from '../../backend/useCreateItem';
|
import { useCreateItem } from '../../backend/useCreateItem';
|
||||||
|
import { IconItemVisibility } from '../../components/IconItemVisibility';
|
||||||
import { SelectAccessPolicy } from '../../components/SelectAccessPolicy';
|
import { SelectAccessPolicy } from '../../components/SelectAccessPolicy';
|
||||||
import { SelectItemType } from '../../components/SelectItemType';
|
import { SelectItemType } from '../../components/SelectItemType';
|
||||||
import { SelectLocationContext } from '../../components/SelectLocationContext';
|
import { SelectLocationContext } from '../../components/SelectLocationContext';
|
||||||
|
@ -69,14 +69,14 @@ export function FormCreateItem() {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
router.push(urls.library);
|
router.push({ path: urls.library });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
setValue('file', event.target.files[0]);
|
setValue('file', event.target.files[0]);
|
||||||
setValue('fileName', event.target.files[0].name);
|
setValue('fileName', event.target.files[0].name, { shouldValidate: true });
|
||||||
} else {
|
} else {
|
||||||
setValue('file', undefined);
|
setValue('file', undefined);
|
||||||
setValue('fileName', '');
|
setValue('fileName', '');
|
||||||
|
@ -88,16 +88,16 @@ export function FormCreateItem() {
|
||||||
setValue('file', undefined);
|
setValue('file', undefined);
|
||||||
setValue('fileName', '');
|
setValue('fileName', '');
|
||||||
}
|
}
|
||||||
setValue('item_type', value);
|
setValue('item_type', value, { shouldValidate: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmit(data: ICreateLibraryItemDTO) {
|
function onSubmit(data: ICreateLibraryItemDTO) {
|
||||||
return createItem(data).then(newItem => {
|
return createItem(data).then(newItem => {
|
||||||
setSearchLocation(data.location);
|
setSearchLocation(data.location);
|
||||||
if (newItem.item_type == LibraryItemType.RSFORM) {
|
if (newItem.item_type == LibraryItemType.RSFORM) {
|
||||||
router.push(urls.schema(newItem.id));
|
router.push({ path: urls.schema(newItem.id), force: true });
|
||||||
} else {
|
} else {
|
||||||
router.push(urls.oss(newItem.id));
|
router.push({ path: urls.oss(newItem.id), force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -188,7 +188,7 @@ export function FormCreateItem() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
icon={<VisibilityIcon value={field.value} />}
|
icon={<IconItemVisibility value={field.value} />}
|
||||||
onClick={() => field.onChange(!field.value)}
|
onClick={() => field.onChange(!field.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -44,9 +44,9 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.item_type === LibraryItemType.RSFORM) {
|
if (item.item_type === LibraryItemType.RSFORM) {
|
||||||
router.push(urls.schema(item.id), event.ctrlKey || event.metaKey);
|
router.push({ path: urls.schema(item.id), newTab: event.ctrlKey || event.metaKey });
|
||||||
} else if (item.item_type === LibraryItemType.OSS) {
|
} else if (item.item_type === LibraryItemType.OSS) {
|
||||||
router.push(urls.oss(item.id), event.ctrlKey || event.metaKey);
|
router.push({ path: urls.oss(item.id), newTab: event.ctrlKey || event.metaKey });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { SelectUser } from '@/features/users';
|
import { SelectUser } from '@/features/users/components';
|
||||||
|
|
||||||
import { MiniButton, SelectorButton } from '@/components/Control';
|
import { MiniButton, SelectorButton } from '@/components/Control';
|
||||||
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
import {
|
import {
|
||||||
IconEditor,
|
IconEditor,
|
||||||
|
@ -20,6 +19,8 @@ import { SearchBar } from '@/components/Input';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { tripleToggleColor } from '@/utils/utils';
|
import { tripleToggleColor } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { IconItemVisibility } from '../../components/IconItemVisibility';
|
||||||
|
import { IconLocationHead } from '../../components/IconLocationHead';
|
||||||
import { describeLocationHead, labelLocationHead } from '../../labels';
|
import { describeLocationHead, labelLocationHead } from '../../labels';
|
||||||
import { LocationHead } from '../../models/library';
|
import { LocationHead } from '../../models/library';
|
||||||
import { useHasCustomFilter, useLibrarySearchStore } from '../../stores/librarySearch';
|
import { useHasCustomFilter, useLibrarySearchStore } from '../../stores/librarySearch';
|
||||||
|
@ -98,7 +99,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Видимость'
|
title='Видимость'
|
||||||
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
|
icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -156,7 +157,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
hideTitle={headMenu.isOpen}
|
hideTitle={headMenu.isOpen}
|
||||||
icon={
|
icon={
|
||||||
head ? (
|
head ? (
|
||||||
<LocationIcon value={head} size='1.25rem' />
|
<IconLocationHead value={head} size='1.25rem' />
|
||||||
) : (
|
) : (
|
||||||
<IconFolderSearch size='1.25rem' className='clr-text-controls' />
|
<IconFolderSearch size='1.25rem' className='clr-text-controls' />
|
||||||
)
|
)
|
||||||
|
@ -165,7 +166,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
text={head ?? '//'}
|
text={head ?? '//'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modalTooltip'>
|
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modal-tooltip'>
|
||||||
<DropdownButton title='Переключение в режим Проводник' onClick={handleToggleFolder}>
|
<DropdownButton title='Переключение в режим Проводник' onClick={handleToggleFolder}>
|
||||||
<div className='inline-flex items-center gap-3'>
|
<div className='inline-flex items-center gap-3'>
|
||||||
<IconFolderTree size='1rem' className='clr-text-controls' />
|
<IconFolderTree size='1rem' className='clr-text-controls' />
|
||||||
|
@ -187,7 +188,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
title={describeLocationHead(head)}
|
title={describeLocationHead(head)}
|
||||||
>
|
>
|
||||||
<div className='inline-flex items-center gap-3'>
|
<div className='inline-flex items-center gap-3'>
|
||||||
<LocationIcon value={head} size='1rem' />
|
<IconLocationHead value={head} size='1rem' />
|
||||||
{labelLocationHead(head)}
|
{labelLocationHead(head)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { toast } from 'react-toastify';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { BadgeHelp, HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { SubfoldersIcon } from '@/components/DomainIcons';
|
|
||||||
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
||||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
|
@ -13,6 +13,7 @@ import { PARAMETER, prefixes } from '@/utils/constants';
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { useLibrary } from '../../backend/useLibrary';
|
import { useLibrary } from '../../backend/useLibrary';
|
||||||
|
import { IconShowSubfolders } from '../../components/IconShowSubfolders';
|
||||||
import { SelectLocation } from '../../components/SelectLocation';
|
import { SelectLocation } from '../../components/SelectLocation';
|
||||||
import { type FolderNode } from '../../models/FolderTree';
|
import { type FolderNode } from '../../models/FolderTree';
|
||||||
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
||||||
|
@ -90,7 +91,7 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
||||||
{!!location ? (
|
{!!location ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
||||||
icon={<SubfoldersIcon value={subfolders} />}
|
icon={<IconShowSubfolders value={subfolders} />}
|
||||||
onClick={toggleSubfolders}
|
onClick={toggleSubfolders}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Module: OSS data loading and processing.
|
* Module: OSS data loading and processing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ILibraryItem } from '@/features/library/backend/types';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
|
|
||||||
import { Graph } from '@/models/Graph';
|
import { Graph } from '@/models/Graph';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { type ILibraryItem } from '@/features/library/backend/types';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { type DomIconProps, IconMoveDown, IconMoveUp } from '@/components/Icons';
|
||||||
|
|
||||||
|
/** Icon for relocation direction. */
|
||||||
|
export function IconRelocationUp({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||||
|
if (value) {
|
||||||
|
return <IconMoveUp size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
} else {
|
||||||
|
return <IconMoveDown size={size} className={className ?? 'text-sec-600'} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Tooltip } from '@/components/Container';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useOperationTooltipStore } from '../stores/operationTooltip';
|
||||||
|
|
||||||
|
import { InfoOperation } from './InfoOperation';
|
||||||
|
|
||||||
|
export function OperationTooltip() {
|
||||||
|
const hoverOperation = useOperationTooltipStore(state => state.activeOperation);
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
id={globalIDs.operation_tooltip}
|
||||||
|
layer='z-topmost'
|
||||||
|
className='max-w-[35rem] max-h-[40rem] dense'
|
||||||
|
hidden={!hoverOperation}
|
||||||
|
>
|
||||||
|
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,9 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { type ILibraryItem, LibraryItemType, PickSchema, useLibrary } from '@/features/library';
|
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||||
|
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||||
|
import { PickSchema } from '@/features/library/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconReset } from '@/components/Icons';
|
import { IconReset } from '@/components/Icons';
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { type ILibraryItem, LibraryItemType, PickSchema, useLibrary } from '@/features/library';
|
import { type ILibraryItem, LibraryItemType } from '@/features/library';
|
||||||
|
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||||
|
import { PickSchema } from '@/features/library/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { IconReset } from '@/components/Icons';
|
import { IconReset } from '@/components/Icons';
|
||||||
|
@ -47,7 +49,7 @@ export function TabInputOperation() {
|
||||||
setValue('create_schema', false);
|
setValue('create_schema', false);
|
||||||
setValue('item_data.alias', schema.alias);
|
setValue('item_data.alias', schema.alias);
|
||||||
setValue('item_data.title', schema.title);
|
setValue('item_data.title', schema.title);
|
||||||
setValue('item_data.comment', schema.comment);
|
setValue('item_data.comment', schema.comment, { shouldValidate: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function DlgEditOperation() {
|
||||||
target: target.id,
|
target: target.id,
|
||||||
item_data: {
|
item_data: {
|
||||||
alias: target.alias,
|
alias: target.alias,
|
||||||
title: target.alias,
|
title: target.title,
|
||||||
comment: target.comment
|
comment: target.comment
|
||||||
},
|
},
|
||||||
arguments: target.arguments,
|
arguments: target.arguments,
|
||||||
|
|
|
@ -17,7 +17,7 @@ export function TabArguments() {
|
||||||
const filtered = oss.items.filter(item => !potentialCycle.includes(item.id));
|
const filtered = oss.items.filter(item => !potentialCycle.includes(item.id));
|
||||||
|
|
||||||
function handleChangeArguments(prev: number[], newValue: number[]) {
|
function handleChangeArguments(prev: number[], newValue: number[]) {
|
||||||
setValue('arguments', newValue);
|
setValue('arguments', newValue, { shouldValidate: true });
|
||||||
if (prev.some(id => !newValue.includes(id))) {
|
if (prev.some(id => !newValue.includes(id))) {
|
||||||
setValue('substitutions', []);
|
setValue('substitutions', []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
import { PickSubstitutions, useRSForms } from '@/features/rsform';
|
import { useRSForms } from '@/features/rsform/backend/useRSForms';
|
||||||
|
import { PickSubstitutions } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { TextArea } from '@/components/Input';
|
import { TextArea } from '@/components/Input';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
|
@ -6,11 +6,13 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { type ILibraryItem, SelectLibraryItem, useLibrary } from '@/features/library';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
import { PickMultiConstituenta, useRSForm } from '@/features/rsform';
|
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||||
|
import { SelectLibraryItem } from '@/features/library/components';
|
||||||
|
import { useRSForm } from '@/features/rsform/backend/useRSForm';
|
||||||
|
import { PickMultiConstituenta } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import { RelocateUpIcon } from '@/components/DomainIcons';
|
|
||||||
import { Loader } from '@/components/Loader';
|
import { Loader } from '@/components/Loader';
|
||||||
import { ModalForm } from '@/components/Modal';
|
import { ModalForm } from '@/components/Modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
@ -18,6 +20,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types';
|
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types';
|
||||||
import { useRelocateConstituents } from '../backend/useRelocateConstituents';
|
import { useRelocateConstituents } from '../backend/useRelocateConstituents';
|
||||||
import { useUpdatePositions } from '../backend/useUpdatePositions';
|
import { useUpdatePositions } from '../backend/useUpdatePositions';
|
||||||
|
import { IconRelocationUp } from '../components/IconRelocationUp';
|
||||||
import { type IOperation, type IOperationSchema } from '../models/oss';
|
import { type IOperation, type IOperationSchema } from '../models/oss';
|
||||||
import { getRelocateCandidates } from '../models/ossAPI';
|
import { getRelocateCandidates } from '../models/ossAPI';
|
||||||
|
|
||||||
|
@ -132,7 +135,7 @@ export function DlgRelocateConstituents() {
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Направление перемещения'
|
title='Направление перемещения'
|
||||||
icon={<RelocateUpIcon value={directionUp} />}
|
icon={<IconRelocationUp value={directionUp} />}
|
||||||
onClick={toggleDirection}
|
onClick={toggleDirection}
|
||||||
/>
|
/>
|
||||||
<SelectLibraryItem
|
<SelectLibraryItem
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export { useFindPredecessor } from './backend/useFindPredecessor';
|
export { type IOperationSchemaDTO } from './backend/types';
|
||||||
|
export { type IOperation } from './models/oss';
|
||||||
|
|
|
@ -2,10 +2,16 @@
|
||||||
* Module: API for OperationSystem.
|
* Module: API for OperationSystem.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ILibraryItem } from '@/features/library/backend/types';
|
import { type ILibraryItem } from '@/features/library';
|
||||||
import { CstType, type ICstSubstitute, ParsingStatus } from '@/features/rsform/backend/types';
|
import {
|
||||||
import { CstClass, type IConstituenta, type IRSForm } from '@/features/rsform/models/rsform';
|
type AliasMapping,
|
||||||
import { type AliasMapping } from '@/features/rsform/models/rslang';
|
CstClass,
|
||||||
|
CstType,
|
||||||
|
type IConstituenta,
|
||||||
|
type ICstSubstitute,
|
||||||
|
type IRSForm,
|
||||||
|
ParsingStatus
|
||||||
|
} from '@/features/rsform';
|
||||||
import {
|
import {
|
||||||
applyAliasMapping,
|
applyAliasMapping,
|
||||||
applyTypificationMapping,
|
applyTypificationMapping,
|
||||||
|
@ -13,7 +19,6 @@ import {
|
||||||
isSetTypification
|
isSetTypification
|
||||||
} from '@/features/rsform/models/rslangAPI';
|
} from '@/features/rsform/models/rslangAPI';
|
||||||
|
|
||||||
import { limits, PARAMETER } from '@/utils/constants';
|
|
||||||
import { infoMsg } from '@/utils/labels';
|
import { infoMsg } from '@/utils/labels';
|
||||||
import { TextMatcher } from '@/utils/utils';
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -24,6 +29,13 @@ import { describeSubstitutionError } from '../labels';
|
||||||
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
|
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
|
||||||
import { type Position2D } from './ossLayout';
|
import { type Position2D } from './ossLayout';
|
||||||
|
|
||||||
|
export const GRID_SIZE = 10; // pixels - size of OSS grid
|
||||||
|
const MIN_DISTANCE = 20; // pixels - minimum distance between node centers
|
||||||
|
const DISTANCE_X = 180; // pixels - insert x-distance between node centers
|
||||||
|
const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
|
||||||
|
|
||||||
|
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given target {@link IOperation} matches the specified query using.
|
* Checks if a given target {@link IOperation} matches the specified query using.
|
||||||
*
|
*
|
||||||
|
@ -92,7 +104,7 @@ export class SubstitutionValidator {
|
||||||
this.schemaByCst.set(item.id, schema);
|
this.schemaByCst.set(item.id, schema);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let index = limits.max_semantic_index;
|
let index = STARTING_SUB_INDEX;
|
||||||
substitutions.forEach(item => {
|
substitutions.forEach(item => {
|
||||||
this.constituents.add(item.original);
|
this.constituents.add(item.original);
|
||||||
this.constituents.add(item.substitution);
|
this.constituents.add(item.substitution);
|
||||||
|
@ -500,27 +512,27 @@ export function calculateInsertPosition(
|
||||||
}
|
}
|
||||||
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
|
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
|
||||||
const minY = Math.min(...inputsNodes.map(node => node.position_y));
|
const minY = Math.min(...inputsNodes.map(node => node.position_y));
|
||||||
result.x = maxX + PARAMETER.ossDistanceX;
|
result.x = maxX + DISTANCE_X;
|
||||||
result.y = minY;
|
result.y = minY;
|
||||||
} else {
|
} else {
|
||||||
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id));
|
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id));
|
||||||
const maxY = Math.max(...argNodes.map(node => node.position_y));
|
const maxY = Math.max(...argNodes.map(node => node.position_y));
|
||||||
const minX = Math.min(...argNodes.map(node => node.position_x));
|
const minX = Math.min(...argNodes.map(node => node.position_x));
|
||||||
const maxX = Math.max(...argNodes.map(node => node.position_x));
|
const maxX = Math.max(...argNodes.map(node => node.position_x));
|
||||||
result.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize;
|
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
|
||||||
result.y = maxY + PARAMETER.ossDistanceY;
|
result.y = maxY + DISTANCE_Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
let flagIntersect = false;
|
let flagIntersect = false;
|
||||||
do {
|
do {
|
||||||
flagIntersect = positions.some(
|
flagIntersect = positions.some(
|
||||||
position =>
|
position =>
|
||||||
Math.abs(position.position_x - result.x) < PARAMETER.ossMinDistance &&
|
Math.abs(position.position_x - result.x) < MIN_DISTANCE &&
|
||||||
Math.abs(position.position_y - result.y) < PARAMETER.ossMinDistance
|
Math.abs(position.position_y - result.y) < MIN_DISTANCE
|
||||||
);
|
);
|
||||||
if (flagIntersect) {
|
if (flagIntersect) {
|
||||||
result.x += PARAMETER.ossMinDistance;
|
result.x += MIN_DISTANCE;
|
||||||
result.y += PARAMETER.ossMinDistance;
|
result.y += MIN_DISTANCE;
|
||||||
}
|
}
|
||||||
} while (flagIntersect);
|
} while (flagIntersect);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { EditorLibraryItem } from '@/features/library';
|
import { EditorLibraryItem } from '@/features/library/components';
|
||||||
import { ToolbarRSFormCard } from '@/features/rsform';
|
import { ToolbarRSFormCard } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { FlexColumn } from '@/components/Container';
|
import { FlexColumn } from '@/components/Container';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
|
@ -6,8 +6,9 @@ import { 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';
|
||||||
|
|
||||||
import { LibraryItemType, ToolbarItemAccess, useUpdateItem } from '@/features/library';
|
import { type IUpdateLibraryItemDTO, LibraryItemType, schemaUpdateLibraryItem } from '@/features/library';
|
||||||
import { type IUpdateLibraryItemDTO, schemaUpdateLibraryItem } from '@/features/library/backend/types';
|
import { useUpdateItem } from '@/features/library/backend/useUpdateItem';
|
||||||
|
import { ToolbarItemAccess } from '@/features/library/components';
|
||||||
|
|
||||||
import { SubmitButton } from '@/components/Control';
|
import { SubmitButton } from '@/components/Control';
|
||||||
import { IconSave } from '@/components/Icons';
|
import { IconSave } from '@/components/Icons';
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||||
|
import { useInputCreate } from '@/features/oss/backend/useInputCreate';
|
||||||
|
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
|
||||||
|
|
||||||
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
import { Dropdown, DropdownButton } from '@/components/Dropdown';
|
||||||
import {
|
import {
|
||||||
|
@ -13,7 +19,8 @@ import {
|
||||||
IconRSForm
|
IconRSForm
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { errorMsg } from '@/utils/labels';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { OperationType } from '../../../backend/types';
|
import { OperationType } from '../../../backend/types';
|
||||||
|
@ -21,8 +28,14 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { type IOperation } from '../../../models/oss';
|
import { type IOperation } from '../../../models/oss';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
|
// pixels - size of OSS context menu
|
||||||
|
const MENU_WIDTH = 200;
|
||||||
|
const MENU_HEIGHT = 200;
|
||||||
|
|
||||||
export interface ContextMenuData {
|
export interface ContextMenuData {
|
||||||
operation: IOperation;
|
operation: IOperation | null;
|
||||||
cursorX: number;
|
cursorX: number;
|
||||||
cursorY: number;
|
cursorY: number;
|
||||||
}
|
}
|
||||||
|
@ -30,33 +43,25 @@ export interface ContextMenuData {
|
||||||
interface NodeContextMenuProps extends ContextMenuData {
|
interface NodeContextMenuProps extends ContextMenuData {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
onDelete: (target: number) => void;
|
|
||||||
onCreateInput: (target: number) => void;
|
|
||||||
onEditSchema: (target: number) => void;
|
|
||||||
onEditOperation: (target: number) => void;
|
|
||||||
onExecuteOperation: (target: number) => void;
|
|
||||||
onRelocateConstituents: (target: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeContextMenu({
|
export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) {
|
||||||
isOpen,
|
const router = useConceptNavigation();
|
||||||
operation,
|
const { items: libraryItems } = useLibrary();
|
||||||
cursorX,
|
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
cursorY,
|
|
||||||
onHide,
|
|
||||||
onDelete,
|
|
||||||
onCreateInput,
|
|
||||||
onEditSchema,
|
|
||||||
onEditOperation,
|
|
||||||
onExecuteOperation,
|
|
||||||
onRelocateConstituents
|
|
||||||
}: NodeContextMenuProps) {
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
const { schema, navigateOperationSchema, isMutable, canDelete } = useOssEdit();
|
const getPositions = useGetPositions();
|
||||||
|
|
||||||
|
const { inputCreate } = useInputCreate();
|
||||||
|
const { operationExecute } = useOperationExecute();
|
||||||
|
|
||||||
|
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
||||||
|
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||||
|
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||||
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const readyForSynthesis = (() => {
|
const readyForSynthesis = (() => {
|
||||||
if (operation.operation_type !== OperationType.SYNTHESIS) {
|
if (operation?.operation_type !== OperationType.SYNTHESIS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (operation.result) {
|
if (operation.result) {
|
||||||
|
@ -76,48 +81,97 @@ export function NodeContextMenu({
|
||||||
return true;
|
return true;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useClickedOutside(isOpen, ref, onHide);
|
useClickedOutside(isOpen, ref, onHide);
|
||||||
|
|
||||||
function handleOpenSchema() {
|
function handleOpenSchema() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onHide();
|
||||||
navigateOperationSchema(operation.id);
|
navigateOperationSchema(operation.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditSchema() {
|
function handleEditSchema() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onEditSchema(operation.id);
|
showEditInput({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditOperation() {
|
function handleEditOperation() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onEditOperation(operation.id);
|
showEditOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteOperation() {
|
function handleDeleteOperation() {
|
||||||
|
if (!operation || !canDelete(operation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onDelete(operation.id);
|
showDeleteOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateSchema() {
|
function handleOperationExecute() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onCreateInput(operation.id);
|
void operationExecute({
|
||||||
|
itemID: schema.id, //
|
||||||
|
data: { target: operation.id, positions: getPositions() }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRunSynthesis() {
|
function handleInputCreate() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
||||||
|
toast.error(errorMsg.inputAlreadyExists);
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onExecuteOperation(operation.id);
|
void inputCreate({
|
||||||
|
itemID: schema.id,
|
||||||
|
data: { target: operation.id, positions: getPositions() }
|
||||||
|
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRelocateConstituents() {
|
function handleRelocateConstituents() {
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
onHide();
|
onHide();
|
||||||
onRelocateConstituents(operation.id);
|
showRelocateConstituents({
|
||||||
|
oss: schema,
|
||||||
|
initialTarget: operation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}>
|
<div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}
|
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
|
||||||
stretchTop={cursorY >= window.innerHeight - PARAMETER.ossContextMenuHeight}
|
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
|
||||||
>
|
>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Редактировать'
|
text='Редактировать'
|
||||||
|
@ -142,7 +196,7 @@ export function NodeContextMenu({
|
||||||
title='Создать пустую схему для загрузки'
|
title='Создать пустую схему для загрузки'
|
||||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleCreateSchema}
|
onClick={handleInputCreate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
||||||
|
@ -164,7 +218,7 @@ export function NodeContextMenu({
|
||||||
}
|
}
|
||||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||||
disabled={isProcessing || !readyForSynthesis}
|
disabled={isProcessing || !readyForSynthesis}
|
||||||
onClick={handleRunSynthesis}
|
onClick={handleOperationExecute}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
@ -181,7 +235,7 @@ export function NodeContextMenu({
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить операцию'
|
text='Удалить операцию'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)}
|
disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
|
||||||
onClick={handleDeleteOperation}
|
onClick={handleDeleteOperation}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
getNodesBounds,
|
|
||||||
getViewportForBounds,
|
|
||||||
type Node,
|
type Node,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
|
@ -13,32 +10,28 @@ import {
|
||||||
useOnSelectionChange,
|
useOnSelectionChange,
|
||||||
useReactFlow
|
useReactFlow
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { toPng } from 'html-to-image';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
|
||||||
import { useLibrary } from '@/features/library';
|
|
||||||
|
|
||||||
import { Overlay } from '@/components/Container';
|
import { Overlay } from '@/components/Container';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
import { useTooltipsStore } from '@/stores/tooltips';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { APP_COLORS } from '@/styling/colors';
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errorMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { useInputCreate } from '../../../backend/useInputCreate';
|
|
||||||
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { useOperationExecute } from '../../../backend/useOperationExecute';
|
|
||||||
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
||||||
|
import { GRID_SIZE } from '../../../models/ossAPI';
|
||||||
import { type OssNode } from '../../../models/ossLayout';
|
import { type OssNode } from '../../../models/ossLayout';
|
||||||
|
import { useOperationTooltipStore } from '../../../stores/operationTooltip';
|
||||||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||||
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
||||||
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
const ZOOM_MAX = 2;
|
const ZOOM_MAX = 2;
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
|
export const VIEW_PADDING = 0.2;
|
||||||
|
|
||||||
export function OssFlow() {
|
export function OssFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
@ -48,35 +41,30 @@ export function OssFlow() {
|
||||||
setSelected,
|
setSelected,
|
||||||
selected,
|
selected,
|
||||||
isMutable,
|
isMutable,
|
||||||
promptCreateOperation,
|
canDeleteOperation: canDelete
|
||||||
canDelete,
|
|
||||||
promptDeleteOperation,
|
|
||||||
promptEditInput,
|
|
||||||
promptEditOperation,
|
|
||||||
promptRelocateConstituents
|
|
||||||
} = useOssEdit();
|
} = useOssEdit();
|
||||||
const router = useConceptNavigation();
|
const { fitView, screenToFlowPosition } = useReactFlow();
|
||||||
const { items: libraryItems } = useLibrary();
|
|
||||||
const flow = useReactFlow();
|
|
||||||
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
const setHoverOperation = useTooltipsStore(state => state.setActiveOperation);
|
const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation);
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
const { inputCreate } = useInputCreate();
|
const getPositions = useGetPositions();
|
||||||
const { operationExecute } = useOperationExecute();
|
|
||||||
const { updatePositions } = useUpdatePositions();
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
const [menuProps, setMenuProps] = useState<ContextMenuData | null>(null);
|
const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||||
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
|
|
||||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||||
const ids = nodes.map(node => Number(node.id));
|
const ids = nodes.map(node => Number(node.id));
|
||||||
setSelected(prev => [
|
setSelected(prev => [
|
||||||
|
@ -112,15 +100,8 @@ export function OssFlow() {
|
||||||
: 'left'
|
: 'left'
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate]);
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
||||||
|
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
||||||
function getPositions() {
|
|
||||||
return nodes.map(node => ({
|
|
||||||
id: Number(node.id),
|
|
||||||
position_x: node.position.x,
|
|
||||||
position_y: node.position.y
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
const positions = getPositions();
|
const positions = getPositions();
|
||||||
|
@ -135,106 +116,34 @@ export function OssFlow() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateOperation(inputs: number[]) {
|
function handleCreateOperation() {
|
||||||
const positions = getPositions();
|
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||||
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
showCreateOperation({
|
||||||
promptCreateOperation({
|
oss: schema,
|
||||||
defaultX: target.x,
|
defaultX: targetPosition.x,
|
||||||
defaultY: target.y,
|
defaultY: targetPosition.y,
|
||||||
inputs: inputs,
|
positions: getPositions(),
|
||||||
positions: positions,
|
initialInputs: selected,
|
||||||
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout)
|
onCreate: () =>
|
||||||
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteOperation(target: number) {
|
|
||||||
if (!canDelete(target)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promptDeleteOperation(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteSelected() {
|
function handleDeleteSelected() {
|
||||||
if (selected.length !== 1) {
|
if (selected.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleDeleteOperation(selected[0]);
|
const operation = schema.operationByID.get(selected[0]);
|
||||||
}
|
if (!operation || !canDelete(operation)) {
|
||||||
|
|
||||||
function handleInputCreate(target: number) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
showDeleteOperation({
|
||||||
toast.error(errorMsg.inputAlreadyExists);
|
oss: schema,
|
||||||
return;
|
target: operation,
|
||||||
}
|
positions: getPositions()
|
||||||
void inputCreate({
|
|
||||||
itemID: schema.id,
|
|
||||||
data: { target: target, positions: getPositions() }
|
|
||||||
}).then(new_schema => router.push(urls.schema(new_schema.id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditSchema(target: number) {
|
|
||||||
promptEditInput(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditOperation(target: number) {
|
|
||||||
promptEditOperation(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOperationExecute(target: number) {
|
|
||||||
void operationExecute({
|
|
||||||
itemID: schema.id, //
|
|
||||||
data: { target: target, positions: getPositions() }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuteSelected() {
|
|
||||||
if (selected.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleOperationExecute(selected[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRelocateConstituents(target: number) {
|
|
||||||
promptRelocateConstituents(target, getPositions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSaveImage() {
|
|
||||||
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
|
||||||
if (canvas === null) {
|
|
||||||
toast.error(errorMsg.imageFailed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageWidth = PARAMETER.ossImageWidth;
|
|
||||||
const imageHeight = PARAMETER.ossImageHeight;
|
|
||||||
const nodesBounds = getNodesBounds(nodes);
|
|
||||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
|
||||||
toPng(canvas, {
|
|
||||||
backgroundColor: APP_COLORS.bgDefault,
|
|
||||||
width: imageWidth,
|
|
||||||
height: imageHeight,
|
|
||||||
style: {
|
|
||||||
width: String(imageWidth),
|
|
||||||
height: String(imageHeight),
|
|
||||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(dataURL => {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute('download', `${schema.alias}.png`);
|
|
||||||
a.setAttribute('href', dataURL);
|
|
||||||
a.click();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error(errorMsg.imageFailed);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -248,14 +157,6 @@ export function OssFlow() {
|
||||||
setHoverOperation(null);
|
setHoverOperation(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextMenuHide() {
|
|
||||||
setIsContextMenuOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCanvasClick() {
|
|
||||||
handleContextMenuHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -265,10 +166,7 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (isProcessing) {
|
if (isProcessing || !isMutable) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isMutable) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||||
|
@ -280,7 +178,7 @@ export function OssFlow() {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleCreateOperation(selected);
|
handleCreateOperation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete') {
|
if (event.key === 'Delete') {
|
||||||
|
@ -298,36 +196,20 @@ export function OssFlow() {
|
||||||
className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50'
|
className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50'
|
||||||
>
|
>
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
onCreate={handleCreateOperation}
|
||||||
onCreate={() => handleCreateOperation(selected)}
|
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
onEdit={() => handleEditOperation(selected[0])}
|
|
||||||
onExecute={handleExecuteSelected}
|
|
||||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||||
onSavePositions={handleSavePositions}
|
|
||||||
onSaveImage={handleSaveImage}
|
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
{menuProps ? (
|
|
||||||
<NodeContextMenu
|
<NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||||
isOpen={isContextMenuOpen}
|
|
||||||
onHide={handleContextMenuHide}
|
|
||||||
onDelete={handleDeleteOperation}
|
|
||||||
onCreateInput={handleInputCreate}
|
|
||||||
onEditSchema={handleEditSchema}
|
|
||||||
onEditOperation={handleEditOperation}
|
|
||||||
onExecuteOperation={handleOperationExecute}
|
|
||||||
onRelocateConstituents={handleRelocateConstituents}
|
|
||||||
{...menuProps}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
|
||||||
edgesFocusable={false}
|
edgesFocusable={false}
|
||||||
nodesFocusable={false}
|
nodesFocusable={false}
|
||||||
fitView
|
fitView
|
||||||
|
@ -336,11 +218,13 @@ export function OssFlow() {
|
||||||
minZoom={ZOOM_MIN}
|
minZoom={ZOOM_MIN}
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
||||||
|
onClick={() => setIsContextMenuOpen(false)}
|
||||||
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onClick={handleCanvasClick}
|
onNodeDragStart={() => setIsContextMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useReactFlow } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { BadgeHelp, HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
|
||||||
|
import { useUpdatePositions } from '@/features/oss/backend/useUpdatePositions';
|
||||||
|
|
||||||
import { MiniButton } from '@/components/Control';
|
import { MiniButton } from '@/components/Control';
|
||||||
import {
|
import {
|
||||||
|
@ -13,13 +17,13 @@ import {
|
||||||
IconExecute,
|
IconExecute,
|
||||||
IconFitImage,
|
IconFitImage,
|
||||||
IconGrid,
|
IconGrid,
|
||||||
IconImage,
|
|
||||||
IconLineStraight,
|
IconLineStraight,
|
||||||
IconLineWave,
|
IconLineWave,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconReset,
|
IconReset,
|
||||||
IconSave
|
IconSave
|
||||||
} from '@/components/Icons';
|
} from '@/components/Icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
@ -28,30 +32,21 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
|
import { VIEW_PADDING } from './OssFlow';
|
||||||
|
import { useGetPositions } from './useGetPositions';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps {
|
interface ToolbarOssGraphProps {
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit: () => void;
|
|
||||||
onExecute: () => void;
|
|
||||||
onFitView: () => void;
|
|
||||||
onSaveImage: () => void;
|
|
||||||
onSavePositions: () => void;
|
|
||||||
onResetPositions: () => void;
|
onResetPositions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolbarOssGraph({
|
export function ToolbarOssGraph({ onCreate, onDelete, onResetPositions }: ToolbarOssGraphProps) {
|
||||||
onCreate,
|
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onExecute,
|
|
||||||
onFitView,
|
|
||||||
onSaveImage,
|
|
||||||
onSavePositions,
|
|
||||||
onResetPositions
|
|
||||||
}: ToolbarOssGraphProps) {
|
|
||||||
const { schema, selected, isMutable, canDelete } = useOssEdit();
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
const selectedOperation = schema.operationByID.get(selected[0]);
|
const selectedOperation = schema.operationByID.get(selected[0]);
|
||||||
|
const getPositions = useGetPositions();
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
|
@ -60,6 +55,11 @@ export function ToolbarOssGraph({
|
||||||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||||
|
|
||||||
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
const { operationExecute } = useOperationExecute();
|
||||||
|
|
||||||
|
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||||
|
|
||||||
const readyForSynthesis = (() => {
|
const readyForSynthesis = (() => {
|
||||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -81,6 +81,44 @@ export function ToolbarOssGraph({
|
||||||
return true;
|
return true;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function handleFitView() {
|
||||||
|
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSavePositions() {
|
||||||
|
const positions = getPositions();
|
||||||
|
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
|
||||||
|
positions.forEach(item => {
|
||||||
|
const operation = schema.operationByID.get(item.id);
|
||||||
|
if (operation) {
|
||||||
|
operation.position_x = item.position_x;
|
||||||
|
operation.position_y = item.position_y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOperationExecute() {
|
||||||
|
if (selected.length !== 1 || !readyForSynthesis || !selectedOperation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void operationExecute({
|
||||||
|
itemID: schema.id, //
|
||||||
|
data: { target: selectedOperation.id, positions: getPositions() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditOperation() {
|
||||||
|
if (selected.length !== 1 || !selectedOperation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showEditOperation({
|
||||||
|
oss: schema,
|
||||||
|
target: selectedOperation,
|
||||||
|
positions: getPositions()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center'>
|
<div className='flex flex-col items-center'>
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
|
@ -92,7 +130,7 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
title='Сбросить вид'
|
title='Сбросить вид'
|
||||||
onClick={onFitView}
|
onClick={handleFitView}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||||
|
@ -127,11 +165,6 @@ export function ToolbarOssGraph({
|
||||||
}
|
}
|
||||||
onClick={toggleEdgeAnimate}
|
onClick={toggleEdgeAnimate}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
|
||||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
|
||||||
title='Сохранить изображение'
|
|
||||||
onClick={onSaveImage}
|
|
||||||
/>
|
|
||||||
<BadgeHelp
|
<BadgeHelp
|
||||||
topic={HelpTopic.UI_OSS_GRAPH}
|
topic={HelpTopic.UI_OSS_GRAPH}
|
||||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||||
|
@ -144,7 +177,7 @@ export function ToolbarOssGraph({
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={onSavePositions}
|
onClick={handleSavePositions}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||||
|
@ -156,18 +189,18 @@ export function ToolbarOssGraph({
|
||||||
title='Активировать операцию'
|
title='Активировать операцию'
|
||||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||||
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
||||||
onClick={onExecute}
|
onClick={handleOperationExecute}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||||
disabled={selected.length !== 1 || isProcessing}
|
disabled={selected.length !== 1 || isProcessing}
|
||||||
onClick={onEdit}
|
onClick={handleEditOperation}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={selected.length !== 1 || isProcessing || !canDelete(selected[0])}
|
disabled={selected.length !== 1 || isProcessing || !selectedOperation || !canDelete(selectedOperation)}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,23 +3,24 @@
|
||||||
import { Overlay } from '@/components/Container';
|
import { Overlay } from '@/components/Container';
|
||||||
import { IconConsolidation, IconRSForm } from '@/components/Icons';
|
import { IconConsolidation, IconRSForm } from '@/components/Icons';
|
||||||
import { Indicator } from '@/components/View';
|
import { Indicator } from '@/components/View';
|
||||||
import { useTooltipsStore } from '@/stores/tooltips';
|
import { globalIDs } from '@/utils/constants';
|
||||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
|
||||||
import { truncateToLastWord } from '@/utils/utils';
|
|
||||||
|
|
||||||
import { OperationType } from '../../../../backend/types';
|
import { OperationType } from '../../../../backend/types';
|
||||||
import { type OssNodeInternal } from '../../../../models/ossLayout';
|
import { type OssNodeInternal } from '../../../../models/ossLayout';
|
||||||
|
import { useOperationTooltipStore } from '../../../../stores/operationTooltip';
|
||||||
|
|
||||||
|
// characters - threshold for long labels - small font
|
||||||
|
const LONG_LABEL_CHARS = 14;
|
||||||
|
|
||||||
interface NodeCoreProps {
|
interface NodeCoreProps {
|
||||||
node: OssNodeInternal;
|
node: OssNodeInternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NodeCore({ node }: NodeCoreProps) {
|
export function NodeCore({ node }: NodeCoreProps) {
|
||||||
const setHover = useTooltipsStore(state => state.setActiveOperation);
|
const setHover = useOperationTooltipStore(state => state.setActiveOperation);
|
||||||
|
|
||||||
const hasFile = !!node.data.operation.result;
|
const hasFile = !!node.data.operation.result;
|
||||||
const longLabel = node.data.label.length > PARAMETER.ossLongLabel;
|
const longLabel = node.data.label.length > LONG_LABEL_CHARS;
|
||||||
const labelText = truncateToLastWord(node.data.label, PARAMETER.ossTruncateLabel);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -53,10 +54,11 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
<div
|
<div
|
||||||
className='h-[34px] w-[144px] flex items-center justify-center'
|
className='h-[34px] w-[144px] flex items-center justify-center'
|
||||||
data-tooltip-id={globalIDs.operation_tooltip}
|
data-tooltip-id={globalIDs.operation_tooltip}
|
||||||
|
data-tooltip-hidden={node.dragging}
|
||||||
onMouseEnter={() => setHover(node.data.operation)}
|
onMouseEnter={() => setHover(node.data.operation)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='text-center'
|
className='text-center line-clamp-2'
|
||||||
style={{
|
style={{
|
||||||
fontSize: longLabel ? '12px' : '14px',
|
fontSize: longLabel ? '12px' : '14px',
|
||||||
lineHeight: longLabel ? '16px' : '20px',
|
lineHeight: longLabel ? '16px' : '20px',
|
||||||
|
@ -64,7 +66,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
||||||
paddingRight: longLabel ? '10px' : '4px'
|
paddingRight: longLabel ? '10px' : '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{labelText}
|
{node.data.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
export function useGetPositions() {
|
||||||
|
const { getNodes } = useReactFlow();
|
||||||
|
return function getPositions() {
|
||||||
|
return getNodes().map(node => ({
|
||||||
|
id: Number(node.id),
|
||||||
|
position_x: node.position.x,
|
||||||
|
position_y: node.position.y
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
import { Button } from '@/components/Control';
|
||||||
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
|
import { IconChild, IconEdit2 } from '@/components/Icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
import { useMutatingOss } from '../../backend/useMutatingOss';
|
||||||
|
|
||||||
|
import { useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
|
export function MenuEditOss() {
|
||||||
|
const { isAnonymous } = useAuthSuspense();
|
||||||
|
const editMenu = useDropdown();
|
||||||
|
const { schema, isMutable } = useOssEdit();
|
||||||
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
|
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||||
|
|
||||||
|
function handleRelocate() {
|
||||||
|
editMenu.hide();
|
||||||
|
showRelocateConstituents({
|
||||||
|
oss: schema,
|
||||||
|
initialTarget: undefined,
|
||||||
|
positions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnonymous) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={editMenu.ref}>
|
||||||
|
<Button
|
||||||
|
dense
|
||||||
|
noBorder
|
||||||
|
noOutline
|
||||||
|
tabIndex={-1}
|
||||||
|
title='Редактирование'
|
||||||
|
hideTitle={editMenu.isOpen}
|
||||||
|
className='h-full px-2'
|
||||||
|
icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
|
||||||
|
onClick={editMenu.toggle}
|
||||||
|
/>
|
||||||
|
<Dropdown isOpen={editMenu.isOpen}>
|
||||||
|
<DropdownButton
|
||||||
|
text='Конституенты'
|
||||||
|
titleHtml='Перенос конституент</br>между схемами'
|
||||||
|
icon={<IconChild size='1rem' className='icon-green' />}
|
||||||
|
disabled={isProcessing}
|
||||||
|
onClick={handleRelocate}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
|
import { Divider } from '@/components/Container';
|
||||||
|
import { Button } from '@/components/Control';
|
||||||
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||||
|
import { IconDestroy, IconLibrary, IconMenu, IconNewItem, IconQR, IconShare } from '@/components/Icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { generatePageQR, sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { useMutatingOss } from '../../backend/useMutatingOss';
|
||||||
|
|
||||||
|
import { useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
|
export function MenuMain() {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { isMutable, deleteSchema } = useOssEdit();
|
||||||
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
|
const { isAnonymous } = useAuthSuspense();
|
||||||
|
|
||||||
|
const role = useRoleStore(state => state.role);
|
||||||
|
|
||||||
|
const showQR = useDialogsStore(state => state.showQR);
|
||||||
|
|
||||||
|
const schemaMenu = useDropdown();
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
schemaMenu.hide();
|
||||||
|
deleteSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShare() {
|
||||||
|
schemaMenu.hide();
|
||||||
|
sharePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateNew() {
|
||||||
|
router.push({ path: urls.create_schema });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShowQR() {
|
||||||
|
schemaMenu.hide();
|
||||||
|
showQR({ target: generatePageQR() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={schemaMenu.ref}>
|
||||||
|
<Button
|
||||||
|
dense
|
||||||
|
noBorder
|
||||||
|
noOutline
|
||||||
|
tabIndex={-1}
|
||||||
|
title='Меню'
|
||||||
|
hideTitle={schemaMenu.isOpen}
|
||||||
|
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
|
||||||
|
className='h-full pl-2'
|
||||||
|
onClick={schemaMenu.toggle}
|
||||||
|
/>
|
||||||
|
<Dropdown isOpen={schemaMenu.isOpen}>
|
||||||
|
<DropdownButton
|
||||||
|
text='Поделиться'
|
||||||
|
icon={<IconShare size='1rem' className='icon-primary' />}
|
||||||
|
onClick={handleShare}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text='QR-код'
|
||||||
|
title='Показать QR-код схемы'
|
||||||
|
icon={<IconQR size='1rem' className='icon-primary' />}
|
||||||
|
onClick={handleShowQR}
|
||||||
|
/>
|
||||||
|
{isMutable ? (
|
||||||
|
<DropdownButton
|
||||||
|
text='Удалить схему'
|
||||||
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
|
disabled={isProcessing || role < UserRole.OWNER}
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Divider margins='mx-3 my-1' />
|
||||||
|
|
||||||
|
{!isAnonymous ? (
|
||||||
|
<DropdownButton
|
||||||
|
text='Создать новую схему'
|
||||||
|
icon={<IconNewItem size='1rem' className='icon-primary' />}
|
||||||
|
onClick={handleCreateNew}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<DropdownButton
|
||||||
|
text='Библиотека'
|
||||||
|
icon={<IconLibrary size='1rem' className='icon-primary' />}
|
||||||
|
onClick={() => router.push({ path: urls.library })}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,213 +1,22 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { MenuRole } from '@/features/library/components';
|
||||||
|
|
||||||
import { Divider } from '@/components/Container';
|
|
||||||
import { Button } from '@/components/Control';
|
|
||||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
|
||||||
import {
|
|
||||||
IconAdmin,
|
|
||||||
IconAlert,
|
|
||||||
IconChild,
|
|
||||||
IconDestroy,
|
|
||||||
IconEdit2,
|
|
||||||
IconEditor,
|
|
||||||
IconLibrary,
|
|
||||||
IconMenu,
|
|
||||||
IconNewItem,
|
|
||||||
IconOwner,
|
|
||||||
IconReader,
|
|
||||||
IconShare
|
|
||||||
} from '@/components/Icons';
|
|
||||||
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
|
|
||||||
import { sharePage } from '@/utils/utils';
|
|
||||||
|
|
||||||
import { useMutatingOss } from '../../backend/useMutatingOss';
|
|
||||||
|
|
||||||
|
import { MenuEditOss } from './MenuEditOss';
|
||||||
|
import { MenuMain } from './MenuMain';
|
||||||
import { useOssEdit } from './OssEditContext';
|
import { useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
export function MenuOssTabs() {
|
export function MenuOssTabs() {
|
||||||
const { deleteSchema, promptRelocateConstituents, isMutable, isOwned, schema } = useOssEdit();
|
const { isOwned, schema } = useOssEdit();
|
||||||
const router = useConceptNavigation();
|
const { user } = useAuthSuspense();
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
|
||||||
|
|
||||||
const isProcessing = useMutatingOss();
|
|
||||||
|
|
||||||
const role = useRoleStore(state => state.role);
|
|
||||||
const setRole = useRoleStore(state => state.setRole);
|
|
||||||
|
|
||||||
const schemaMenu = useDropdown();
|
|
||||||
const editMenu = useDropdown();
|
|
||||||
const accessMenu = useDropdown();
|
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
schemaMenu.hide();
|
|
||||||
deleteSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleShare() {
|
|
||||||
schemaMenu.hide();
|
|
||||||
sharePage();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChangeRole(newMode: UserRole) {
|
|
||||||
accessMenu.hide();
|
|
||||||
setRole(newMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreateNew() {
|
|
||||||
router.push(urls.create_schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLogin() {
|
|
||||||
router.push(urls.login);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRelocate() {
|
|
||||||
editMenu.hide();
|
|
||||||
promptRelocateConstituents(undefined, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex border-r-2'>
|
<div className='flex border-r-2'>
|
||||||
<div ref={schemaMenu.ref}>
|
<MenuMain />
|
||||||
<Button
|
|
||||||
dense
|
|
||||||
noBorder
|
|
||||||
noOutline
|
|
||||||
tabIndex={-1}
|
|
||||||
title='Меню'
|
|
||||||
hideTitle={schemaMenu.isOpen}
|
|
||||||
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
|
|
||||||
className='h-full pl-2'
|
|
||||||
onClick={schemaMenu.toggle}
|
|
||||||
/>
|
|
||||||
<Dropdown isOpen={schemaMenu.isOpen}>
|
|
||||||
<DropdownButton
|
|
||||||
text='Поделиться'
|
|
||||||
icon={<IconShare size='1rem' className='icon-primary' />}
|
|
||||||
onClick={handleShare}
|
|
||||||
/>
|
|
||||||
{isMutable ? (
|
|
||||||
<DropdownButton
|
|
||||||
text='Удалить схему'
|
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
|
||||||
disabled={isProcessing || role < UserRole.OWNER}
|
|
||||||
onClick={handleDelete}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Divider margins='mx-3 my-1' />
|
<MenuEditOss />
|
||||||
|
|
||||||
{!isAnonymous ? (
|
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
|
||||||
<DropdownButton
|
|
||||||
text='Создать новую схему'
|
|
||||||
icon={<IconNewItem size='1rem' className='icon-primary' />}
|
|
||||||
onClick={handleCreateNew}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<DropdownButton
|
|
||||||
text='Библиотека'
|
|
||||||
icon={<IconLibrary size='1rem' className='icon-primary' />}
|
|
||||||
onClick={() => router.push(urls.library)}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isAnonymous ? (
|
|
||||||
<div ref={editMenu.ref}>
|
|
||||||
<Button
|
|
||||||
dense
|
|
||||||
noBorder
|
|
||||||
noOutline
|
|
||||||
tabIndex={-1}
|
|
||||||
title='Редактирование'
|
|
||||||
hideTitle={editMenu.isOpen}
|
|
||||||
className='h-full px-2'
|
|
||||||
icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
|
|
||||||
onClick={editMenu.toggle}
|
|
||||||
/>
|
|
||||||
<Dropdown isOpen={editMenu.isOpen}>
|
|
||||||
<DropdownButton
|
|
||||||
text='Конституенты'
|
|
||||||
titleHtml='Перенос конституент</br>между схемами'
|
|
||||||
icon={<IconChild size='1rem' className='icon-green' />}
|
|
||||||
disabled={isProcessing}
|
|
||||||
onClick={handleRelocate}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isAnonymous ? (
|
|
||||||
<div ref={accessMenu.ref}>
|
|
||||||
<Button
|
|
||||||
dense
|
|
||||||
noBorder
|
|
||||||
noOutline
|
|
||||||
tabIndex={-1}
|
|
||||||
title={`Режим ${labelUserRole(role)}`}
|
|
||||||
hideTitle={accessMenu.isOpen}
|
|
||||||
className='h-full pr-2'
|
|
||||||
icon={
|
|
||||||
role === UserRole.ADMIN ? (
|
|
||||||
<IconAdmin size='1.25rem' className='icon-primary' />
|
|
||||||
) : role === UserRole.OWNER ? (
|
|
||||||
<IconOwner size='1.25rem' className='icon-primary' />
|
|
||||||
) : role === UserRole.EDITOR ? (
|
|
||||||
<IconEditor size='1.25rem' className='icon-primary' />
|
|
||||||
) : (
|
|
||||||
<IconReader size='1.25rem' className='icon-primary' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={accessMenu.toggle}
|
|
||||||
/>
|
|
||||||
<Dropdown isOpen={accessMenu.isOpen}>
|
|
||||||
<DropdownButton
|
|
||||||
text={labelUserRole(UserRole.READER)}
|
|
||||||
title={describeUserRole(UserRole.READER)}
|
|
||||||
icon={<IconReader size='1rem' className='icon-primary' />}
|
|
||||||
onClick={() => handleChangeRole(UserRole.READER)}
|
|
||||||
/>
|
|
||||||
<DropdownButton
|
|
||||||
text={labelUserRole(UserRole.EDITOR)}
|
|
||||||
title={describeUserRole(UserRole.EDITOR)}
|
|
||||||
icon={<IconEditor size='1rem' className='icon-primary' />}
|
|
||||||
disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
|
|
||||||
onClick={() => handleChangeRole(UserRole.EDITOR)}
|
|
||||||
/>
|
|
||||||
<DropdownButton
|
|
||||||
text={labelUserRole(UserRole.OWNER)}
|
|
||||||
title={describeUserRole(UserRole.OWNER)}
|
|
||||||
icon={<IconOwner size='1rem' className='icon-primary' />}
|
|
||||||
disabled={!isOwned}
|
|
||||||
onClick={() => handleChangeRole(UserRole.OWNER)}
|
|
||||||
/>
|
|
||||||
<DropdownButton
|
|
||||||
text={labelUserRole(UserRole.ADMIN)}
|
|
||||||
title={describeUserRole(UserRole.ADMIN)}
|
|
||||||
icon={<IconAdmin size='1rem' className='icon-primary' />}
|
|
||||||
disabled={!user.is_staff}
|
|
||||||
onClick={() => handleChangeRole(UserRole.ADMIN)}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{isAnonymous ? (
|
|
||||||
<Button
|
|
||||||
dense
|
|
||||||
noBorder
|
|
||||||
noOutline
|
|
||||||
tabIndex={-1}
|
|
||||||
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
|
||||||
hideTitle={accessMenu.isOpen}
|
|
||||||
className='h-full pr-2'
|
|
||||||
icon={<IconAlert size='1.25rem' className='icon-red' />}
|
|
||||||
onClick={handleLogin}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,17 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useDeleteItem, useLibrarySearchStore } from '@/features/library';
|
import { useLibrarySearchStore } from '@/features/library';
|
||||||
|
import { useDeleteItem } from '@/features/library/backend/useDeleteItem';
|
||||||
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
|
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
import { type IOperationPosition, OperationType } from '../../backend/types';
|
import { type IOperationPosition, OperationType } from '../../backend/types';
|
||||||
import { useOssSuspense } from '../../backend/useOSS';
|
import { useOssSuspense } from '../../backend/useOSS';
|
||||||
import { type IOperationSchema } from '../../models/oss';
|
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||||
|
|
||||||
export enum OssTabID {
|
export enum OssTabID {
|
||||||
CARD = 0,
|
CARD = 0,
|
||||||
|
@ -39,15 +39,9 @@ export interface IOssEditContext {
|
||||||
navigateTab: (tab: OssTabID) => void;
|
navigateTab: (tab: OssTabID) => void;
|
||||||
navigateOperationSchema: (target: number) => void;
|
navigateOperationSchema: (target: number) => void;
|
||||||
|
|
||||||
|
canDeleteOperation: (target: IOperation) => boolean;
|
||||||
deleteSchema: () => void;
|
deleteSchema: () => void;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||||
|
|
||||||
canDelete: (target: number) => boolean;
|
|
||||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
|
||||||
promptDeleteOperation: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptEditInput: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptEditOperation: (target: number, positions: IOperationPosition[]) => void;
|
|
||||||
promptRelocateConstituents: (target: number | undefined, positions: IOperationPosition[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||||
|
@ -80,12 +74,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
|
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
|
||||||
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
|
||||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
|
||||||
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
|
||||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
|
||||||
|
|
||||||
const { deleteItem } = useDeleteItem();
|
const { deleteItem } = useDeleteItem();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -104,7 +92,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
tab: tab
|
tab: tab
|
||||||
});
|
});
|
||||||
router.push(url);
|
router.push({ path: url });
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateOperationSchema(target: number) {
|
function navigateOperationSchema(target: number) {
|
||||||
|
@ -112,86 +100,29 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
if (!node?.result) {
|
if (!node?.result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }));
|
router.push({ path: urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSchema() {
|
function deleteSchema() {
|
||||||
if (!window.confirm(promptText.deleteOSS)) {
|
if (!window.confirm(promptText.deleteOSS)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void deleteItem(schema.id).then(() => {
|
void deleteItem({
|
||||||
if (searchLocation === schema.location) {
|
target: schema.id,
|
||||||
setSearchLocation('');
|
beforeInvalidate: () => {
|
||||||
|
if (searchLocation === schema.location) {
|
||||||
|
setSearchLocation('');
|
||||||
|
}
|
||||||
|
return router.pushAsync({ path: urls.library, force: true });
|
||||||
}
|
}
|
||||||
router.push(urls.library);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
|
function canDeleteOperation(target: IOperation) {
|
||||||
showCreateOperation({
|
if (target.operation_type === OperationType.INPUT) {
|
||||||
oss: schema,
|
|
||||||
defaultX: defaultX,
|
|
||||||
defaultY: defaultY,
|
|
||||||
positions: positions,
|
|
||||||
initialInputs: inputs,
|
|
||||||
onCreate: callback
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function canDelete(target: number) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (operation.operation_type === OperationType.INPUT) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return schema.graph.expandOutputs([target]).length === 0;
|
return schema.graph.expandOutputs([target.id]).length === 0;
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditOperation(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditOperation({
|
|
||||||
oss: schema,
|
|
||||||
target: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptDeleteOperation(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showDeleteOperation({
|
|
||||||
oss: schema,
|
|
||||||
positions: positions,
|
|
||||||
target: operation
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditInput(target: number, positions: IOperationPosition[]) {
|
|
||||||
const operation = schema.operationByID.get(target);
|
|
||||||
if (!operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditInput({
|
|
||||||
oss: schema,
|
|
||||||
target: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptRelocateConstituents(target: number | undefined, positions: IOperationPosition[]) {
|
|
||||||
const operation = target ? schema.operationByID.get(target) : undefined;
|
|
||||||
showRelocateConstituents({
|
|
||||||
oss: schema,
|
|
||||||
initialTarget: operation,
|
|
||||||
positions: positions
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -200,22 +131,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
schema,
|
schema,
|
||||||
selected,
|
selected,
|
||||||
|
|
||||||
navigateTab,
|
|
||||||
|
|
||||||
deleteSchema,
|
|
||||||
|
|
||||||
isOwned,
|
isOwned,
|
||||||
isMutable,
|
isMutable,
|
||||||
|
|
||||||
setSelected,
|
navigateTab,
|
||||||
|
|
||||||
navigateOperationSchema,
|
navigateOperationSchema,
|
||||||
promptCreateOperation,
|
|
||||||
canDelete,
|
canDeleteOperation,
|
||||||
promptDeleteOperation,
|
deleteSchema,
|
||||||
promptEditInput,
|
setSelected
|
||||||
promptEditOperation,
|
|
||||||
promptRelocateConstituents
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useParams } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { urls, useBlockNavigation, useConceptNavigation } from '@/app';
|
import { urls, useBlockNavigation, useConceptNavigation } from '@/app';
|
||||||
|
import { ConstituentaTooltip } from '@/features/rsform/components';
|
||||||
|
|
||||||
import { isAxiosError } from '@/backend/apiTransport';
|
import { isAxiosError } from '@/backend/apiTransport';
|
||||||
import { TextURL } from '@/components/Control';
|
import { TextURL } from '@/components/Control';
|
||||||
|
@ -13,6 +14,8 @@ import { type ErrorData } from '@/components/InfoError';
|
||||||
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
|
import { OperationTooltip } from '../../components/OperationTooltip';
|
||||||
|
|
||||||
import { OssEditState, OssTabID } from './OssEditContext';
|
import { OssEditState, OssTabID } from './OssEditContext';
|
||||||
import { OssTabs } from './OssTabs';
|
import { OssTabs } from './OssTabs';
|
||||||
|
|
||||||
|
@ -37,12 +40,14 @@ export function OssPage() {
|
||||||
useEffect(() => setIsModified(false), [setIsModified]);
|
useEffect(() => setIsModified(false), [setIsModified]);
|
||||||
|
|
||||||
if (!urlData.id) {
|
if (!urlData.id) {
|
||||||
router.replace(urls.page404);
|
router.replace({ path: urls.page404, force: true });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ProcessError}>
|
<ErrorBoundary FallbackComponent={ProcessError}>
|
||||||
|
<OperationTooltip />
|
||||||
|
<ConstituentaTooltip />
|
||||||
<OssEditState itemID={urlData.id}>
|
<OssEditState itemID={urlData.id}>
|
||||||
<OssTabs activeTab={urlData.tab} />
|
<OssTabs activeTab={urlData.tab} />
|
||||||
</OssEditState>
|
</OssEditState>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { type IOperation } from '../models/oss';
|
||||||
|
|
||||||
|
interface OperationTooltipStore {
|
||||||
|
activeOperation: IOperation | null;
|
||||||
|
setActiveOperation: (value: IOperation | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOperationTooltipStore = create<OperationTooltipStore>()(set => ({
|
||||||
|
activeOperation: null,
|
||||||
|
setActiveOperation: value => set({ activeOperation: value })
|
||||||
|
}));
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useUpdateTimestamp } from '@/features/library';
|
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||||
|
|
||||||
import { KEYS } from '@/backend/configuration';
|
import { KEYS } from '@/backend/configuration';
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user