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
|
||||
- use-debounce
|
||||
- qrcode.react
|
||||
- html-to-image
|
||||
- zustand
|
||||
- zod
|
||||
- @hookform/resolvers
|
||||
|
|
5
TODO.txt
5
TODO.txt
|
@ -8,9 +8,10 @@ For more specific TODOs see comments in code
|
|||
- Landing page
|
||||
- Design first user experience
|
||||
- Demo sandbox for anonymous users
|
||||
- Save react-flow to vector image
|
||||
|
||||
User profile:
|
||||
- Settings + settings server persistency
|
||||
- Settings server persistency
|
||||
- Profile pictures
|
||||
- Custom LibraryItem lists
|
||||
- Custom user filters and sharing filters
|
||||
|
@ -39,7 +40,6 @@ User profile:
|
|||
|
||||
[Tech]
|
||||
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
|
||||
- Testing E2E playwright
|
||||
|
||||
|
||||
[Deployment]
|
||||
|
@ -60,7 +60,6 @@ Research and consider integration
|
|||
- skeleton loading
|
||||
https://react.dev/reference/react/Suspense
|
||||
|
||||
- backend error message unification
|
||||
- drf-messages
|
||||
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": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.0",
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-query": "^5.66.8",
|
||||
"@tanstack/react-query-devtools": "^5.66.8",
|
||||
"@tanstack/react-query": "^5.66.9",
|
||||
"@tanstack/react-query-devtools": "^5.66.9",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@uiw/codemirror-themes": "^4.23.8",
|
||||
"@uiw/react-codemirror": "^4.23.8",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.1",
|
||||
"clsx": "^2.1.1",
|
||||
"global": "^4.4.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"js-file-download": "^0.4.12",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
|
@ -34,10 +33,10 @@
|
|||
"react-icons": "^5.5.0",
|
||||
"react-intl": "^7.1.6",
|
||||
"react-router": "^7.2.0",
|
||||
"react-scan": "^0.1.3",
|
||||
"react-scan": "^0.1.4",
|
||||
"react-select": "^5.10.0",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-toastify": "^11.0.3",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"reactflow": "^11.11.4",
|
||||
|
@ -48,16 +47,16 @@
|
|||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.2",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@tailwindcss/vite": "^4.0.7",
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"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-react": "^7.37.4",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216",
|
||||
|
@ -66,10 +65,10 @@
|
|||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest": "^29.2.6",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.1.1"
|
||||
"typescript-eslint": "^8.25.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "^19.0.0"
|
||||
|
|
|
@ -27,7 +27,7 @@ export function ApplicationLayout() {
|
|||
<NavigationState>
|
||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||
<ToasterThemed
|
||||
className='text-[14px] cc-animate-position'
|
||||
className='text-[14px]'
|
||||
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
|
||||
autoClose={3000}
|
||||
draggable={false}
|
||||
|
|
|
@ -1,24 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
import { Tooltip } from '@/components/Container';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
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 = () => {
|
||||
const hoverCst = useTooltipsStore(state => state.activeCst);
|
||||
const hoverOperation = useTooltipsStore(state => state.activeOperation);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
|
@ -34,27 +19,6 @@ export const GlobalTooltips = () => {
|
|||
layer='z-topmost'
|
||||
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 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>) =>
|
||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
||||
const navigateHelp = (event: React.MouseEvent<Element>) => router.push(urls.manuals, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.library, newTab: 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>) =>
|
||||
router.push(urls.create_schema, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.create_schema, newTab: event.ctrlKey || event.metaKey });
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
@ -42,9 +44,10 @@ export function Navigation() {
|
|||
'cc-shadow-border'
|
||||
)}
|
||||
style={{
|
||||
transitionProperty: 'height, translate',
|
||||
willChange: 'max-height, translate',
|
||||
transitionProperty: 'max-height, translate',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
height: noNavigationAnimation ? '0rem' : '3rem',
|
||||
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
|
||||
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -3,13 +3,19 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
export interface NavigationProps {
|
||||
path: string;
|
||||
newTab?: boolean;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
interface INavigationContext {
|
||||
push: (path: string, newTab?: boolean) => void;
|
||||
replace: (path: string) => void;
|
||||
back: () => void;
|
||||
forward: () => void;
|
||||
push: (props: NavigationProps) => void;
|
||||
pushAsync: (props: NavigationProps) => void | Promise<void>;
|
||||
replace: (props: Omit<NavigationProps, 'newTab'>) => void;
|
||||
replaceAsync: (props: Omit<NavigationProps, 'newTab'>) => void | Promise<void>;
|
||||
back: (force?: boolean) => void;
|
||||
forward: (force?: boolean) => void;
|
||||
|
||||
canBack: () => boolean;
|
||||
|
||||
|
@ -21,7 +27,7 @@ const NavigationContext = createContext<INavigationContext | null>(null);
|
|||
export const useConceptNavigation = () => {
|
||||
const context = useContext(NavigationContext);
|
||||
if (!context) {
|
||||
throw new Error(contextOutsideScope('useConceptNavigation', 'NavigationState'));
|
||||
throw new Error('useConceptNavigation has to be used within <NavigationState>');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
@ -39,33 +45,47 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
return !!window.history && window.history?.length !== 0;
|
||||
}
|
||||
|
||||
function push(path: string, newTab?: boolean) {
|
||||
if (newTab) {
|
||||
window.open(`${path}`, '_blank');
|
||||
return;
|
||||
}
|
||||
if (validate()) {
|
||||
Promise.resolve(router(path, { viewTransition: true })).catch(console.error);
|
||||
function push(props: NavigationProps) {
|
||||
if (props.newTab) {
|
||||
window.open(`${props.path}`, '_blank');
|
||||
} else if (props.force || validate()) {
|
||||
setIsBlocked(false);
|
||||
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
function replace(path: string) {
|
||||
if (validate()) {
|
||||
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.error);
|
||||
function pushAsync(props: NavigationProps): void | Promise<void> {
|
||||
if (props.newTab) {
|
||||
window.open(`${props.path}`, '_blank');
|
||||
} else if (props.force || validate()) {
|
||||
setIsBlocked(false);
|
||||
return router(props.path, { viewTransition: true });
|
||||
}
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (validate()) {
|
||||
function replace(props: Omit<NavigationProps, 'newTab'>) {
|
||||
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);
|
||||
setIsBlocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
function forward() {
|
||||
if (validate()) {
|
||||
function forward(force?: boolean) {
|
||||
if (force || validate()) {
|
||||
Promise.resolve(router(1)).catch(console.error);
|
||||
setIsBlocked(false);
|
||||
}
|
||||
|
@ -75,7 +95,9 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
|
|||
<NavigationContext
|
||||
value={{
|
||||
push,
|
||||
pushAsync,
|
||||
replace,
|
||||
replaceAsync,
|
||||
back,
|
||||
forward,
|
||||
canBack,
|
||||
|
|
|
@ -22,7 +22,8 @@ export function ToggleNavigation() {
|
|||
!noNavigation && 'flex-col-reverse'
|
||||
)}
|
||||
style={{
|
||||
transitionProperty: 'height, width, background-color',
|
||||
willChange: 'height, width',
|
||||
transitionProperty: 'height, width',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
height: noNavigationAnimation ? '2rem' : '3rem',
|
||||
width: noNavigationAnimation ? '3rem' : '2rem'
|
||||
|
@ -32,7 +33,7 @@ export function ToggleNavigation() {
|
|||
<button
|
||||
tabIndex={-1}
|
||||
type='button'
|
||||
className='p-1'
|
||||
className='p-1 cursor-pointer'
|
||||
onClick={toggleDarkMode}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
|
||||
|
@ -44,7 +45,7 @@ export function ToggleNavigation() {
|
|||
<button
|
||||
tabIndex={-1}
|
||||
type='button'
|
||||
className='p-1'
|
||||
className='p-1 cursor-pointer'
|
||||
onClick={toggleNoNavigation}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
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 {
|
||||
|
@ -40,32 +41,32 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
|||
|
||||
function navigateProfile(event: React.MouseEvent<Element>) {
|
||||
hideDropdown();
|
||||
router.push(urls.profile, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.profile, newTab: event.ctrlKey || event.metaKey });
|
||||
}
|
||||
|
||||
function logoutAndRedirect() {
|
||||
hideDropdown();
|
||||
void logout().then(() => router.push(urls.login));
|
||||
void logout().then(() => router.push({ path: urls.login, force: true }));
|
||||
}
|
||||
|
||||
function gotoAdmin() {
|
||||
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>) {
|
||||
hideDropdown();
|
||||
router.push(urls.icons, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.icons, newTab: event.ctrlKey || event.metaKey });
|
||||
}
|
||||
|
||||
function gotoRestApi() {
|
||||
hideDropdown();
|
||||
router.push(urls.rest_api, true);
|
||||
router.push({ path: urls.rest_api, newTab: true });
|
||||
}
|
||||
|
||||
function gotoDatabaseSchema(event: React.MouseEvent<Element>) {
|
||||
hideDropdown();
|
||||
router.push(urls.database_schema, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.database_schema, newTab: event.ctrlKey || event.metaKey });
|
||||
}
|
||||
|
||||
function handleToggleDarkMode() {
|
||||
|
|
|
@ -15,7 +15,7 @@ export function UserMenu() {
|
|||
return (
|
||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
||||
<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>
|
||||
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
|
|||
import { type z, ZodError } from 'zod';
|
||||
|
||||
import { buildConstants } from '@/utils/buildConstants';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { extractErrorMessage } from '@/utils/utils';
|
||||
|
||||
|
@ -62,11 +63,7 @@ export function axiosGet<ResponseData>({ endpoint, options, schema }: IAxiosGetR
|
|||
.catch((error: Error | AxiosError) => {
|
||||
// Note: Ignore cancellation errors
|
||||
if (error.name !== 'CanceledError') {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
notifyError(error);
|
||||
console.error(error);
|
||||
}
|
||||
throw error;
|
||||
|
@ -83,21 +80,11 @@ export function axiosPost<RequestData, ResponseData = void>({
|
|||
.post<ResponseData>(endpoint, request?.data, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
} else {
|
||||
toast.success(request.successMessage(response.data));
|
||||
}
|
||||
}
|
||||
notifySuccess(response.data, request?.successMessage);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
notifyError(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
@ -112,21 +99,11 @@ export function axiosDelete<RequestData, ResponseData = void>({
|
|||
.delete<ResponseData>(endpoint, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
} else {
|
||||
toast.success(request.successMessage(response.data));
|
||||
}
|
||||
}
|
||||
notifySuccess(response.data, request?.successMessage);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
notifyError(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
@ -141,21 +118,36 @@ export function axiosPatch<RequestData, ResponseData = void>({
|
|||
.patch<ResponseData>(endpoint, request?.data, options)
|
||||
.then(response => {
|
||||
schema?.parse(response.data);
|
||||
if (request?.successMessage) {
|
||||
if (typeof request.successMessage === 'string') {
|
||||
toast.success(request.successMessage);
|
||||
} else {
|
||||
toast.success(request.successMessage(response.data));
|
||||
}
|
||||
}
|
||||
notifySuccess(response.data, request?.successMessage);
|
||||
return response.data;
|
||||
})
|
||||
.catch((error: Error | AxiosError | ZodError) => {
|
||||
if (error instanceof ZodError) {
|
||||
toast.error(errorMsg.invalidResponse);
|
||||
} else {
|
||||
toast.error(extractErrorMessage(error));
|
||||
}
|
||||
notifyError(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
|
||||
)}
|
||||
style={{
|
||||
willChange: 'clip-path, transform',
|
||||
transitionProperty: 'clip-path, transform',
|
||||
transitionDuration: `${PARAMETER.dropdownDuration}ms`,
|
||||
transitionTimingFunction: 'ease-in-out',
|
||||
|
|
|
@ -155,6 +155,10 @@ export { LuCircleDashed as IconAnimation } from 'react-icons/lu';
|
|||
export { LuCircle as IconAnimationOff } from 'react-icons/lu';
|
||||
|
||||
// ===== Custom elements ======
|
||||
export interface DomIconProps<RequestData> extends IconProps {
|
||||
value: RequestData;
|
||||
}
|
||||
|
||||
interface IconSVGProps {
|
||||
viewBox: string;
|
||||
size?: string;
|
||||
|
|
|
@ -98,11 +98,12 @@ export function SelectTree<ItemType>({
|
|||
onClick={event => handleSetValue(event, item)}
|
||||
style={{
|
||||
borderBottomWidth: isActive ? '1px' : '0px',
|
||||
transitionProperty: 'height, opacity, padding',
|
||||
willChange: 'max-height, opacity, padding',
|
||||
transitionProperty: 'max-height, opacity, padding',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
paddingTop: isActive ? '0.25rem' : '0',
|
||||
paddingBottom: isActive ? '0.25rem' : '0',
|
||||
height: isActive ? 'min-content' : '0',
|
||||
maxHeight: isActive ? '1.75rem' : '0',
|
||||
opacity: isActive ? '1' : '0'
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
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 { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { Overlay } from '../Container';
|
||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
import { type Styling } from '../props';
|
||||
|
@ -103,13 +105,15 @@ export function ModalForm({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
<Overlay className='z-modalOverlay'>
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</Overlay>
|
||||
|
||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||
|
||||
|
@ -127,7 +131,7 @@ export function ModalForm({
|
|||
{children}
|
||||
</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
|
||||
autoFocus
|
||||
text={submitText}
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { BadgeHelp } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components';
|
||||
|
||||
import { useEscapeKey } from '@/hooks/useEscapeKey';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { Overlay } from '../Container';
|
||||
import { Button, MiniButton } from '../Control';
|
||||
import { IconClose } from '../Icons';
|
||||
|
||||
|
@ -48,13 +49,15 @@ export function ModalView({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
<Overlay className='z-modalOverlay'>
|
||||
<MiniButton
|
||||
noPadding
|
||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||
icon={<IconClose size='1.25rem' />}
|
||||
className='float-right mt-2 mr-2'
|
||||
onClick={hideDialog}
|
||||
/>
|
||||
</Overlay>
|
||||
|
||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||
|
||||
|
@ -72,7 +75,7 @@ export function ModalView({
|
|||
{children}
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { type Styling, type Titled } from '@/components/props';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { ValueIcon } from './ValueIcon';
|
||||
|
||||
// characters - threshold for small labels - small font
|
||||
const SMALL_THRESHOLD = 3;
|
||||
|
||||
interface ValueStatsProps extends Styling, Titled {
|
||||
/** Id of the component. */
|
||||
id: string;
|
||||
|
@ -18,5 +20,5 @@ interface ValueStatsProps extends Styling, Titled {
|
|||
* Displays statistics value with an icon.
|
||||
*/
|
||||
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();
|
||||
|
||||
function logoutAndRedirect() {
|
||||
void logout().then(() => router.push(urls.login));
|
||||
void logout().then(() => router.push({ path: urls.login, force: true }));
|
||||
}
|
||||
|
||||
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 { 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()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push(urls.library);
|
||||
router.push({ path: urls.library, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ export function Component() {
|
|||
password: newPassword,
|
||||
token: token
|
||||
}).then(() => {
|
||||
router.replace(urls.home);
|
||||
router.push(urls.login);
|
||||
router.replace({ path: urls.home });
|
||||
router.push({ path: urls.login });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpPro
|
|||
return (
|
||||
<div tabIndex={-1} id={`help-${topic}`} className={padding}>
|
||||
<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 />}>
|
||||
<div className='relative' onClick={event => event.stopPropagation()}>
|
||||
<div className='absolute right-0 text-sm top-[0.4rem] clr-input'>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { CstClass } from '@/features/rsform';
|
||||
import { colorBgCstClass } from '@/features/rsform/colors';
|
||||
import { describeCstClass, labelCstClass } from '@/features/rsform/labels';
|
||||
import { CstClass } from '@/features/rsform/models/rsform';
|
||||
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { ExpressionStatus } from '@/features/rsform';
|
||||
import { colorBgCstStatus } from '@/features/rsform/colors';
|
||||
import { describeExpressionStatus, labelExpressionStatus } from '@/features/rsform/labels';
|
||||
import { ExpressionStatus } from '@/features/rsform/models/rsform';
|
||||
|
||||
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';
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconImage,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconNewItem,
|
||||
|
@ -82,9 +81,6 @@ export function HelpOssGraph() {
|
|||
<li>
|
||||
<IconSave className='inline-icon' /> Сохранить положения
|
||||
</li>
|
||||
<li>
|
||||
<IconImage className='inline-icon' /> Сохранить в SVG
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
IconGraphInputs,
|
||||
IconGraphMaximize,
|
||||
IconGraphOutputs,
|
||||
IconImage,
|
||||
IconNewItem,
|
||||
IconOSS,
|
||||
IconPredecessor,
|
||||
|
@ -85,9 +84,6 @@ export function HelpRSGraphTerm() {
|
|||
<IconTypeGraph className='inline-icon' /> Открыть{' '}
|
||||
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
|
||||
</li>
|
||||
<li>
|
||||
<IconImage className='inline-icon' /> Сохранить в формат PNG
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<Divider vertical margins='mx-3' className='hidden sm:block' />
|
||||
|
|
|
@ -4,7 +4,6 @@ import { urls, useConceptNavigation } from '@/app';
|
|||
|
||||
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { HelpTopic } from '../../models/helpTopic';
|
||||
|
||||
|
@ -19,13 +18,11 @@ export function ManualsPage() {
|
|||
const mainHeight = useMainHeight();
|
||||
|
||||
function onSelectTopic(newTopic: HelpTopic) {
|
||||
router.push(urls.help_topic(newTopic));
|
||||
router.push({ path: urls.help_topic(newTopic) });
|
||||
}
|
||||
|
||||
if (!Object.values(HelpTopic).includes(activeTopic)) {
|
||||
setTimeout(() => {
|
||||
router.push(urls.page404);
|
||||
}, PARAMETER.refreshTimeout);
|
||||
router.push({ path: urls.page404, force: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
|||
className={clsx(
|
||||
'absolute left-0 w-[13.5rem]', // prettier: split-lines
|
||||
'flex flex-col',
|
||||
'z-modalTooltip',
|
||||
'z-modal-tooltip',
|
||||
'text-xs sm:text-sm',
|
||||
'select-none',
|
||||
{
|
||||
|
@ -66,6 +66,7 @@ export function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownPro
|
|||
)}
|
||||
style={{
|
||||
maxHeight: treeHeight,
|
||||
willChange: 'clip-path',
|
||||
transitionProperty: 'clip-path',
|
||||
transitionDuration: `${PARAMETER.moveDuration}ms`,
|
||||
clipPath: menu.isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(0% 100% 0% 0%)'
|
||||
|
|
|
@ -6,9 +6,9 @@ export function HomePage() {
|
|||
const { isAnonymous } = useAuthSuspense();
|
||||
|
||||
if (isAnonymous) {
|
||||
router.replace(urls.manuals);
|
||||
router.replace({ path: urls.manuals });
|
||||
} else {
|
||||
router.replace(urls.library);
|
||||
router.replace({ path: urls.library });
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
type IVersionCreatedResponse,
|
||||
schemaRSForm,
|
||||
schemaVersionCreatedResponse
|
||||
} from '@/features/rsform/backend/types';
|
||||
} from '@/features/rsform';
|
||||
|
||||
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||
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({
|
||||
endpoint: `/api/library/${target}`,
|
||||
endpoint: `/api/library/${data.target}`,
|
||||
request: {
|
||||
successMessage: infoMsg.itemDestroyed
|
||||
}
|
||||
|
|
|
@ -10,22 +10,23 @@ export const useDeleteItem = () => {
|
|||
const mutation = useMutation({
|
||||
mutationKey: [KEYS.global_mutation, libraryApi.baseKey, 'delete-item'],
|
||||
mutationFn: libraryApi.deleteItem,
|
||||
onSuccess: (_, variables) => {
|
||||
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }).catch(console.error);
|
||||
onSuccess: async (_, variables) => {
|
||||
await client.invalidateQueries({ queryKey: libraryApi.libraryListKey });
|
||||
await Promise.resolve(variables.beforeInvalidate?.());
|
||||
setTimeout(
|
||||
() =>
|
||||
void Promise.allSettled([
|
||||
client.invalidateQueries({ queryKey: [KEYS.oss] }),
|
||||
client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables }) }),
|
||||
client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables }) })
|
||||
]).catch(console.error),
|
||||
PARAMETER.navigationDuration
|
||||
client.resetQueries({ queryKey: KEYS.composite.rsItem({ itemID: variables.target }) }),
|
||||
client.resetQueries({ queryKey: KEYS.composite.ossItem({ itemID: variables.target }) })
|
||||
]),
|
||||
PARAMETER.refreshTimeout
|
||||
);
|
||||
},
|
||||
onError: () => client.invalidateQueries()
|
||||
});
|
||||
return {
|
||||
deleteItem: (target: number) => mutation.mutateAsync(target),
|
||||
deleteItem: (data: { target: number; beforeInvalidate?: () => void | Promise<void> }) => mutation.mutateAsync(data),
|
||||
isPending: mutation.isPending
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||
import { type IRSFormDTO } from '@/features/rsform';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||
import { type IRSFormDTO } from '@/features/rsform';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||
import { type IRSFormDTO } from '@/features/rsform';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||
import { type IRSFormDTO } from '@/features/rsform';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { type IOperationSchemaDTO } from '@/features/oss/backend/types';
|
||||
import { type IRSFormDTO } from '@/features/rsform/backend/types';
|
||||
import { type IOperationSchemaDTO } from '@/features/oss';
|
||||
import { type IRSFormDTO } from '@/features/rsform';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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';
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { LocationIcon } from '@/components/DomainIcons';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { IconLocationHead } from './IconLocationHead';
|
||||
|
||||
interface BadgeLocationProps {
|
||||
/** Location to display. */
|
||||
location: string;
|
||||
|
@ -12,7 +13,7 @@ interface BadgeLocationProps {
|
|||
export function BadgeLocation({ location }: BadgeLocationProps) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ import { Suspense } from 'react';
|
|||
import { useIntl } from 'react-intl';
|
||||
|
||||
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 { MiniButton } from '@/components/Control';
|
||||
|
@ -63,7 +64,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
|||
|
||||
function handleOpenLibrary(event: React.MouseEvent<Element>) {
|
||||
setGlobalLocation(schema.location);
|
||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
||||
router.push({ path: urls.library, newTab: event.ctrlKey || event.metaKey });
|
||||
}
|
||||
|
||||
function handleEditLocation() {
|
||||
|
@ -125,7 +126,7 @@ export function EditorLibraryItem({ schema, isAttachedToOSS }: EditorLibraryItem
|
|||
onClick={handleEditEditors}
|
||||
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} />}>
|
||||
<InfoUsers items={schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
|
||||
</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 {
|
||||
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) {
|
||||
const ossMenu = useDropdown();
|
||||
|
||||
function onToggle(event: React.MouseEvent<Element>) {
|
||||
function onToggle(event: React.MouseEvent<HTMLElement>) {
|
||||
if (items.length > 1) {
|
||||
ossMenu.toggle();
|
||||
} else {
|
||||
|
|
|
@ -120,7 +120,7 @@ export function PickSchema({
|
|||
className='mt-1'
|
||||
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
|
||||
value={filterLocation}
|
||||
prefix={prefixes.folders_list}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
'use client';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { type DomIconProps } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { AccessPolicy } from '../backend/types';
|
||||
import { describeAccessPolicy, labelAccessPolicy } from '../labels';
|
||||
|
||||
import { IconAccessPolicy } from './IconAccessPolicy';
|
||||
|
||||
interface SelectAccessPolicyProps extends Styling {
|
||||
value: AccessPolicy;
|
||||
onChange: (value: AccessPolicy) => void;
|
||||
|
@ -34,7 +34,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
|||
title={`Доступ: ${labelAccessPolicy(value)}`}
|
||||
hideTitle={menu.isOpen}
|
||||
className='h-full'
|
||||
icon={<PolicyIcon value={value} size='1.25rem' />}
|
||||
icon={<IconAccessPolicy value={value} size='1.25rem' />}
|
||||
onClick={menu.toggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
@ -44,7 +44,7 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
|||
key={`${prefixes.policy_list}${index}`}
|
||||
text={labelAccessPolicy(item)}
|
||||
title={describeAccessPolicy(item)}
|
||||
icon={<PolicyIcon value={item} size='1rem' />}
|
||||
icon={<IconAccessPolicy value={item} size='1rem' />}
|
||||
onClick={() => handleChange(item)}
|
||||
/>
|
||||
))}
|
||||
|
@ -52,15 +52,3 @@ export function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...
|
|||
</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';
|
||||
|
||||
import { SelectorButton } from '@/components/Control';
|
||||
import { type DomIconProps } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { IconOSS, IconRSForm } from '@/components/Icons';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import { LibraryItemType } from '../backend/types';
|
||||
import { describeLibraryItemType, labelLibraryItemType } from '../labels';
|
||||
|
||||
import { IconLibraryItemType } from './IconLibraryItemType';
|
||||
|
||||
interface SelectItemTypeProps extends Styling {
|
||||
value: LibraryItemType;
|
||||
onChange: (value: LibraryItemType) => void;
|
||||
|
@ -34,7 +34,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
|||
title={describeLibraryItemType(value)}
|
||||
hideTitle={menu.isOpen}
|
||||
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)}
|
||||
onClick={menu.toggle}
|
||||
disabled={disabled}
|
||||
|
@ -45,7 +45,7 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
|||
key={`${prefixes.policy_list}${index}`}
|
||||
text={labelLibraryItemType(item)}
|
||||
title={describeLibraryItemType(item)}
|
||||
icon={<ItemTypeIcon value={item} size='1rem' />}
|
||||
icon={<IconLibraryItemType value={item} size='1rem' />}
|
||||
onClick={() => handleChange(item)}
|
||||
/>
|
||||
))}
|
||||
|
@ -53,13 +53,3 @@ export function SelectItemType({ value, disabled, stretchLeft, onChange, ...rest
|
|||
</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
|
||||
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}
|
||||
>
|
||||
<SelectLocation
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { SelectorButton } from '@/components/Control';
|
||||
import { LocationIcon } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { type Styling } from '@/components/props';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
@ -11,6 +10,8 @@ import { prefixes } from '@/utils/constants';
|
|||
import { describeLocationHead, labelLocationHead } from '../labels';
|
||||
import { LocationHead } from '../models/library';
|
||||
|
||||
import { IconLocationHead } from './IconLocationHead';
|
||||
|
||||
interface SelectLocationHeadProps extends Styling {
|
||||
value: LocationHead;
|
||||
onChange: (newValue: LocationHead) => void;
|
||||
|
@ -39,12 +40,12 @@ export function SelectLocationHead({
|
|||
title={describeLocationHead(value)}
|
||||
hideTitle={menu.isOpen}
|
||||
className='h-full'
|
||||
icon={<LocationIcon value={value} size='1rem' />}
|
||||
icon={<IconLocationHead value={value} size='1rem' />}
|
||||
text={labelLocationHead(value)}
|
||||
onClick={menu.toggle}
|
||||
/>
|
||||
|
||||
<Dropdown isOpen={menu.isOpen} className='z-modalTooltip'>
|
||||
<Dropdown isOpen={menu.isOpen} className='z-modal-tooltip'>
|
||||
{Object.values(LocationHead)
|
||||
.filter(head => !excluded.includes(head))
|
||||
.map((head, index) => {
|
||||
|
@ -56,7 +57,7 @@ export function SelectLocationHead({
|
|||
title={describeLocationHead(head)}
|
||||
>
|
||||
<div className='inline-flex items-center gap-3'>
|
||||
<LocationIcon value={head} size='1rem' />
|
||||
<IconLocationHead value={head} size='1rem' />
|
||||
{labelLocationHead(head)}
|
||||
</div>
|
||||
</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 { Overlay } from '@/components/Container';
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||
import { Label } from '@/components/Input';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
@ -12,6 +12,7 @@ import { type AccessPolicy, type ILibraryItem } from '../backend/types';
|
|||
import { useMutatingLibrary } from '../backend/useMutatingLibrary';
|
||||
import { useSetAccessPolicy } from '../backend/useSetAccessPolicy';
|
||||
|
||||
import { IconItemVisibility } from './IconItemVisibility';
|
||||
import { SelectAccessPolicy } from './SelectAccessPolicy';
|
||||
|
||||
interface ToolbarItemAccessProps {
|
||||
|
@ -52,7 +53,7 @@ export function ToolbarItemAccess({
|
|||
|
||||
<MiniButton
|
||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||
icon={<VisibilityIcon value={visible} />}
|
||||
icon={<IconItemVisibility value={visible} />}
|
||||
onClick={toggleVisible}
|
||||
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 { MiniButton } from '@/components/Control';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { Checkbox, Label, TextArea, TextInput } from '@/components/Input';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { AccessPolicy, type ICloneLibraryItemDTO, type ILibraryItem, schemaCloneLibraryItem } from '../backend/types';
|
||||
import { useCloneItem } from '../backend/useCloneItem';
|
||||
import { IconItemVisibility } from '../components/IconItemVisibility';
|
||||
import { SelectAccessPolicy } from '../components/SelectAccessPolicy';
|
||||
import { SelectLocationContext } from '../components/SelectLocationContext';
|
||||
import { SelectLocationHead } from '../components/SelectLocationHead';
|
||||
|
@ -60,7 +60,7 @@ export function DlgCloneLibraryItem() {
|
|||
});
|
||||
|
||||
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 (
|
||||
|
@ -105,7 +105,7 @@ export function DlgCloneLibraryItem() {
|
|||
render={({ field }) => (
|
||||
<MiniButton
|
||||
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||
icon={<VisibilityIcon value={field.value} />}
|
||||
icon={<IconItemVisibility value={field.value} />}
|
||||
onClick={() => field.onChange(!field.value)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
import { useState } from 'react';
|
||||
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 { IconRemove } from '@/components/Icons';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useMemo } from 'react';
|
|||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { useRSFormSuspense } from '@/features/rsform';
|
||||
import { useRSFormSuspense } from '@/features/rsform/backend/useRSForm';
|
||||
|
||||
import { MiniButton } from '@/components/Control';
|
||||
import { IconReset, IconSave } from '@/components/Icons';
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
export { AccessPolicy, type ILibraryItem, type IVersionInfo, LibraryItemType } from './backend/types';
|
||||
export { useDeleteItem } from './backend/useDeleteItem';
|
||||
export { useLibrary, useLibrarySuspense } from './backend/useLibrary';
|
||||
export { useMutatingLibrary } from './backend/useMutatingLibrary';
|
||||
export { useTemplatesSuspense } from './backend/useTemplates';
|
||||
export { useUpdateItem } from './backend/useUpdateItem';
|
||||
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
|
||||
export { useVersionRestore } from './backend/useVersionRestore';
|
||||
export { EditorLibraryItem } from './components/EditorLibraryItem';
|
||||
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
|
||||
export { PickSchema } from './components/PickSchema';
|
||||
export { SelectLibraryItem } from './components/SelectLibraryItem';
|
||||
export { SelectVersion } from './components/SelectVersion';
|
||||
export { ToolbarItemAccess } from './components/ToolbarItemAccess';
|
||||
export { type ILibraryItemReference } from './models/library';
|
||||
export {
|
||||
AccessPolicy,
|
||||
type ILibraryItem,
|
||||
type ILibraryItemData,
|
||||
type IUpdateLibraryItemDTO,
|
||||
type IVersionInfo,
|
||||
LibraryItemType,
|
||||
schemaLibraryItem,
|
||||
schemaUpdateLibraryItem,
|
||||
schemaVersionInfo
|
||||
} from './backend/types';
|
||||
export { BASIC_SCHEMAS, type CurrentVersion, type ILibraryItemReference, LocationHead } from './models/library';
|
||||
export { useLibrarySearchStore } from './stores/librarySearch';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RequireAuth } from '@/features/auth';
|
||||
import { RequireAuth } from '@/features/auth/components';
|
||||
|
||||
import { FormCreateItem } from './FormCreateItem';
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import { useAuthSuspense } from '@/features/auth';
|
|||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { Button, MiniButton, SubmitButton } from '@/components/Control';
|
||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { IconDownload } from '@/components/Icons';
|
||||
import { InfoError } from '@/components/InfoError';
|
||||
import { Label, TextArea, TextInput } from '@/components/Input';
|
||||
|
@ -23,6 +22,7 @@ import {
|
|||
schemaCreateLibraryItem
|
||||
} from '../../backend/types';
|
||||
import { useCreateItem } from '../../backend/useCreateItem';
|
||||
import { IconItemVisibility } from '../../components/IconItemVisibility';
|
||||
import { SelectAccessPolicy } from '../../components/SelectAccessPolicy';
|
||||
import { SelectItemType } from '../../components/SelectItemType';
|
||||
import { SelectLocationContext } from '../../components/SelectLocationContext';
|
||||
|
@ -69,14 +69,14 @@ export function FormCreateItem() {
|
|||
if (router.canBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push(urls.library);
|
||||
router.push({ path: urls.library });
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setValue('file', event.target.files[0]);
|
||||
setValue('fileName', event.target.files[0].name);
|
||||
setValue('fileName', event.target.files[0].name, { shouldValidate: true });
|
||||
} else {
|
||||
setValue('file', undefined);
|
||||
setValue('fileName', '');
|
||||
|
@ -88,16 +88,16 @@ export function FormCreateItem() {
|
|||
setValue('file', undefined);
|
||||
setValue('fileName', '');
|
||||
}
|
||||
setValue('item_type', value);
|
||||
setValue('item_type', value, { shouldValidate: true });
|
||||
}
|
||||
|
||||
function onSubmit(data: ICreateLibraryItemDTO) {
|
||||
return createItem(data).then(newItem => {
|
||||
setSearchLocation(data.location);
|
||||
if (newItem.item_type == LibraryItemType.RSFORM) {
|
||||
router.push(urls.schema(newItem.id));
|
||||
router.push({ path: urls.schema(newItem.id), force: true });
|
||||
} 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 }) => (
|
||||
<MiniButton
|
||||
title={field.value ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||
icon={<VisibilityIcon value={field.value} />}
|
||||
icon={<IconItemVisibility value={field.value} />}
|
||||
onClick={() => field.onChange(!field.value)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -44,9 +44,9 @@ export function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
|||
return;
|
||||
}
|
||||
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) {
|
||||
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 { SelectUser } from '@/features/users';
|
||||
import { SelectUser } from '@/features/users/components';
|
||||
|
||||
import { MiniButton, SelectorButton } from '@/components/Control';
|
||||
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import {
|
||||
IconEditor,
|
||||
|
@ -20,6 +19,8 @@ import { SearchBar } from '@/components/Input';
|
|||
import { prefixes } from '@/utils/constants';
|
||||
import { tripleToggleColor } from '@/utils/utils';
|
||||
|
||||
import { IconItemVisibility } from '../../components/IconItemVisibility';
|
||||
import { IconLocationHead } from '../../components/IconLocationHead';
|
||||
import { describeLocationHead, labelLocationHead } from '../../labels';
|
||||
import { LocationHead } from '../../models/library';
|
||||
import { useHasCustomFilter, useLibrarySearchStore } from '../../stores/librarySearch';
|
||||
|
@ -98,7 +99,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
|||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
title='Видимость'
|
||||
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
|
||||
icon={<IconItemVisibility value={true} className={tripleToggleColor(isVisible)} />}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
|
||||
|
@ -156,7 +157,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
|||
hideTitle={headMenu.isOpen}
|
||||
icon={
|
||||
head ? (
|
||||
<LocationIcon value={head} size='1.25rem' />
|
||||
<IconLocationHead value={head} size='1.25rem' />
|
||||
) : (
|
||||
<IconFolderSearch size='1.25rem' className='clr-text-controls' />
|
||||
)
|
||||
|
@ -165,7 +166,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
|||
text={head ?? '//'}
|
||||
/>
|
||||
|
||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modalTooltip'>
|
||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modal-tooltip'>
|
||||
<DropdownButton title='Переключение в режим Проводник' onClick={handleToggleFolder}>
|
||||
<div className='inline-flex items-center gap-3'>
|
||||
<IconFolderTree size='1rem' className='clr-text-controls' />
|
||||
|
@ -187,7 +188,7 @@ export function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
|||
title={describeLocationHead(head)}
|
||||
>
|
||||
<div className='inline-flex items-center gap-3'>
|
||||
<LocationIcon value={head} size='1rem' />
|
||||
<IconLocationHead value={head} size='1rem' />
|
||||
{labelLocationHead(head)}
|
||||
</div>
|
||||
</DropdownButton>
|
||||
|
|
|
@ -2,10 +2,10 @@ import { toast } from 'react-toastify';
|
|||
import clsx from 'clsx';
|
||||
|
||||
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 { SubfoldersIcon } from '@/components/DomainIcons';
|
||||
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
||||
import { useWindowSize } from '@/hooks/useWindowSize';
|
||||
import { useFitHeight } from '@/stores/appLayout';
|
||||
|
@ -13,6 +13,7 @@ import { PARAMETER, prefixes } from '@/utils/constants';
|
|||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
import { useLibrary } from '../../backend/useLibrary';
|
||||
import { IconShowSubfolders } from '../../components/IconShowSubfolders';
|
||||
import { SelectLocation } from '../../components/SelectLocation';
|
||||
import { type FolderNode } from '../../models/FolderTree';
|
||||
import { useLibrarySearchStore } from '../../stores/librarySearch';
|
||||
|
@ -90,7 +91,7 @@ export function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocati
|
|||
{!!location ? (
|
||||
<MiniButton
|
||||
title={subfolders ? 'Вложенные папки: Вкл' : 'Вложенные папки: Выкл'} // prettier: split-lines
|
||||
icon={<SubfoldersIcon value={subfolders} />}
|
||||
icon={<IconShowSubfolders value={subfolders} />}
|
||||
onClick={toggleSubfolders}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* 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';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
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 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 { IconReset } from '@/components/Icons';
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
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 { IconReset } from '@/components/Icons';
|
||||
|
@ -47,7 +49,7 @@ export function TabInputOperation() {
|
|||
setValue('create_schema', false);
|
||||
setValue('item_data.alias', schema.alias);
|
||||
setValue('item_data.title', schema.title);
|
||||
setValue('item_data.comment', schema.comment);
|
||||
setValue('item_data.comment', schema.comment, { shouldValidate: true });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -47,7 +47,7 @@ export function DlgEditOperation() {
|
|||
target: target.id,
|
||||
item_data: {
|
||||
alias: target.alias,
|
||||
title: target.alias,
|
||||
title: target.title,
|
||||
comment: target.comment
|
||||
},
|
||||
arguments: target.arguments,
|
||||
|
|
|
@ -17,7 +17,7 @@ export function TabArguments() {
|
|||
const filtered = oss.items.filter(item => !potentialCycle.includes(item.id));
|
||||
|
||||
function handleChangeArguments(prev: number[], newValue: number[]) {
|
||||
setValue('arguments', newValue);
|
||||
setValue('arguments', newValue, { shouldValidate: true });
|
||||
if (prev.some(id => !newValue.includes(id))) {
|
||||
setValue('substitutions', []);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
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 { useDialogsStore } from '@/stores/dialogs';
|
||||
|
|
|
@ -6,11 +6,13 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { type ILibraryItem, SelectLibraryItem, useLibrary } from '@/features/library';
|
||||
import { PickMultiConstituenta, useRSForm } from '@/features/rsform';
|
||||
import { type ILibraryItem } from '@/features/library';
|
||||
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 { RelocateUpIcon } from '@/components/DomainIcons';
|
||||
import { Loader } from '@/components/Loader';
|
||||
import { ModalForm } from '@/components/Modal';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
@ -18,6 +20,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
|||
import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types';
|
||||
import { useRelocateConstituents } from '../backend/useRelocateConstituents';
|
||||
import { useUpdatePositions } from '../backend/useUpdatePositions';
|
||||
import { IconRelocationUp } from '../components/IconRelocationUp';
|
||||
import { type IOperation, type IOperationSchema } from '../models/oss';
|
||||
import { getRelocateCandidates } from '../models/ossAPI';
|
||||
|
||||
|
@ -132,7 +135,7 @@ export function DlgRelocateConstituents() {
|
|||
/>
|
||||
<MiniButton
|
||||
title='Направление перемещения'
|
||||
icon={<RelocateUpIcon value={directionUp} />}
|
||||
icon={<IconRelocationUp value={directionUp} />}
|
||||
onClick={toggleDirection}
|
||||
/>
|
||||
<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.
|
||||
*/
|
||||
|
||||
import { type ILibraryItem } from '@/features/library/backend/types';
|
||||
import { CstType, type ICstSubstitute, ParsingStatus } from '@/features/rsform/backend/types';
|
||||
import { CstClass, type IConstituenta, type IRSForm } from '@/features/rsform/models/rsform';
|
||||
import { type AliasMapping } from '@/features/rsform/models/rslang';
|
||||
import { type ILibraryItem } from '@/features/library';
|
||||
import {
|
||||
type AliasMapping,
|
||||
CstClass,
|
||||
CstType,
|
||||
type IConstituenta,
|
||||
type ICstSubstitute,
|
||||
type IRSForm,
|
||||
ParsingStatus
|
||||
} from '@/features/rsform';
|
||||
import {
|
||||
applyAliasMapping,
|
||||
applyTypificationMapping,
|
||||
|
@ -13,7 +19,6 @@ import {
|
|||
isSetTypification
|
||||
} from '@/features/rsform/models/rslangAPI';
|
||||
|
||||
import { limits, PARAMETER } from '@/utils/constants';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
import { TextMatcher } from '@/utils/utils';
|
||||
|
||||
|
@ -24,6 +29,13 @@ import { describeSubstitutionError } from '../labels';
|
|||
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
|
||||
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.
|
||||
*
|
||||
|
@ -92,7 +104,7 @@ export class SubstitutionValidator {
|
|||
this.schemaByCst.set(item.id, schema);
|
||||
});
|
||||
});
|
||||
let index = limits.max_semantic_index;
|
||||
let index = STARTING_SUB_INDEX;
|
||||
substitutions.forEach(item => {
|
||||
this.constituents.add(item.original);
|
||||
this.constituents.add(item.substitution);
|
||||
|
@ -500,27 +512,27 @@ export function calculateInsertPosition(
|
|||
}
|
||||
const maxX = Math.max(...inputsNodes.map(node => node.position_x));
|
||||
const minY = Math.min(...inputsNodes.map(node => node.position_y));
|
||||
result.x = maxX + PARAMETER.ossDistanceX;
|
||||
result.x = maxX + DISTANCE_X;
|
||||
result.y = minY;
|
||||
} else {
|
||||
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id));
|
||||
const maxY = Math.max(...argNodes.map(node => node.position_y));
|
||||
const minX = Math.min(...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.y = maxY + PARAMETER.ossDistanceY;
|
||||
result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
|
||||
result.y = maxY + DISTANCE_Y;
|
||||
}
|
||||
|
||||
let flagIntersect = false;
|
||||
do {
|
||||
flagIntersect = positions.some(
|
||||
position =>
|
||||
Math.abs(position.position_x - result.x) < PARAMETER.ossMinDistance &&
|
||||
Math.abs(position.position_y - result.y) < PARAMETER.ossMinDistance
|
||||
Math.abs(position.position_x - result.x) < MIN_DISTANCE &&
|
||||
Math.abs(position.position_y - result.y) < MIN_DISTANCE
|
||||
);
|
||||
if (flagIntersect) {
|
||||
result.x += PARAMETER.ossMinDistance;
|
||||
result.y += PARAMETER.ossMinDistance;
|
||||
result.x += MIN_DISTANCE;
|
||||
result.y += MIN_DISTANCE;
|
||||
}
|
||||
} while (flagIntersect);
|
||||
return result;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { EditorLibraryItem } from '@/features/library';
|
||||
import { ToolbarRSFormCard } from '@/features/rsform';
|
||||
import { EditorLibraryItem } from '@/features/library/components';
|
||||
import { ToolbarRSFormCard } from '@/features/rsform/components';
|
||||
|
||||
import { FlexColumn } from '@/components/Container';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
|
|
@ -6,8 +6,9 @@ import { useForm, useWatch } from 'react-hook-form';
|
|||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { LibraryItemType, ToolbarItemAccess, useUpdateItem } from '@/features/library';
|
||||
import { type IUpdateLibraryItemDTO, schemaUpdateLibraryItem } from '@/features/library/backend/types';
|
||||
import { type IUpdateLibraryItemDTO, LibraryItemType, schemaUpdateLibraryItem } from '@/features/library';
|
||||
import { useUpdateItem } from '@/features/library/backend/useUpdateItem';
|
||||
import { ToolbarItemAccess } from '@/features/library/components';
|
||||
|
||||
import { SubmitButton } from '@/components/Control';
|
||||
import { IconSave } from '@/components/Icons';
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
'use client';
|
||||
|
||||
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 {
|
||||
|
@ -13,7 +19,8 @@ import {
|
|||
IconRSForm
|
||||
} from '@/components/Icons';
|
||||
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 { OperationType } from '../../../backend/types';
|
||||
|
@ -21,8 +28,14 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
|||
import { type IOperation } from '../../../models/oss';
|
||||
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 {
|
||||
operation: IOperation;
|
||||
operation: IOperation | null;
|
||||
cursorX: number;
|
||||
cursorY: number;
|
||||
}
|
||||
|
@ -30,33 +43,25 @@ export interface ContextMenuData {
|
|||
interface NodeContextMenuProps extends ContextMenuData {
|
||||
isOpen: boolean;
|
||||
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({
|
||||
isOpen,
|
||||
operation,
|
||||
cursorX,
|
||||
cursorY,
|
||||
onHide,
|
||||
onDelete,
|
||||
onCreateInput,
|
||||
onEditSchema,
|
||||
onEditOperation,
|
||||
onExecuteOperation,
|
||||
onRelocateConstituents
|
||||
}: NodeContextMenuProps) {
|
||||
export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) {
|
||||
const router = useConceptNavigation();
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
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 = (() => {
|
||||
if (operation.operation_type !== OperationType.SYNTHESIS) {
|
||||
if (operation?.operation_type !== OperationType.SYNTHESIS) {
|
||||
return false;
|
||||
}
|
||||
if (operation.result) {
|
||||
|
@ -76,48 +81,97 @@ export function NodeContextMenu({
|
|||
return true;
|
||||
})();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useClickedOutside(isOpen, ref, onHide);
|
||||
|
||||
function handleOpenSchema() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
navigateOperationSchema(operation.id);
|
||||
}
|
||||
|
||||
function handleEditSchema() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onEditSchema(operation.id);
|
||||
showEditInput({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditOperation() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onEditOperation(operation.id);
|
||||
showEditOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteOperation() {
|
||||
if (!operation || !canDelete(operation)) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onDelete(operation.id);
|
||||
showDeleteOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateSchema() {
|
||||
function handleOperationExecute() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
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() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onRelocateConstituents(operation.id);
|
||||
showRelocateConstituents({
|
||||
oss: schema,
|
||||
initialTarget: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}>
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}
|
||||
stretchTop={cursorY >= window.innerHeight - PARAMETER.ossContextMenuHeight}
|
||||
stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
|
||||
stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
|
||||
>
|
||||
<DropdownButton
|
||||
text='Редактировать'
|
||||
|
@ -142,7 +196,7 @@ export function NodeContextMenu({
|
|||
title='Создать пустую схему для загрузки'
|
||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||
disabled={isProcessing}
|
||||
onClick={handleCreateSchema}
|
||||
onClick={handleInputCreate}
|
||||
/>
|
||||
) : null}
|
||||
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
||||
|
@ -164,7 +218,7 @@ export function NodeContextMenu({
|
|||
}
|
||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||
disabled={isProcessing || !readyForSynthesis}
|
||||
onClick={handleRunSynthesis}
|
||||
onClick={handleOperationExecute}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -181,7 +235,7 @@ export function NodeContextMenu({
|
|||
<DropdownButton
|
||||
text='Удалить операцию'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)}
|
||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
|
||||
onClick={handleDeleteOperation}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Background,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
type Node,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
|
@ -13,32 +10,28 @@ import {
|
|||
useOnSelectionChange,
|
||||
useReactFlow
|
||||
} from 'reactflow';
|
||||
import { toPng } from 'html-to-image';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useLibrary } from '@/features/library';
|
||||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import { useInputCreate } from '../../../backend/useInputCreate';
|
||||
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||
import { useOperationExecute } from '../../../backend/useOperationExecute';
|
||||
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
||||
import { GRID_SIZE } from '../../../models/ossAPI';
|
||||
import { type OssNode } from '../../../models/ossLayout';
|
||||
import { useOperationTooltipStore } from '../../../stores/operationTooltip';
|
||||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
||||
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
||||
import { useGetPositions } from './useGetPositions';
|
||||
|
||||
const ZOOM_MAX = 2;
|
||||
const ZOOM_MIN = 0.5;
|
||||
export const VIEW_PADDING = 0.2;
|
||||
|
||||
export function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
|
@ -48,35 +41,30 @@ export function OssFlow() {
|
|||
setSelected,
|
||||
selected,
|
||||
isMutable,
|
||||
promptCreateOperation,
|
||||
canDelete,
|
||||
promptDeleteOperation,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
promptRelocateConstituents
|
||||
canDeleteOperation: canDelete
|
||||
} = useOssEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const flow = useReactFlow();
|
||||
const { fitView, screenToFlowPosition } = useReactFlow();
|
||||
|
||||
const isProcessing = useMutatingOss();
|
||||
|
||||
const setHoverOperation = useTooltipsStore(state => state.setActiveOperation);
|
||||
const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation);
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
|
||||
const { inputCreate } = useInputCreate();
|
||||
const { operationExecute } = useOperationExecute();
|
||||
const getPositions = useGetPositions();
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
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 showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
|
||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
setSelected(prev => [
|
||||
|
@ -112,15 +100,8 @@ export function OssFlow() {
|
|||
: 'left'
|
||||
}))
|
||||
);
|
||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate]);
|
||||
|
||||
function getPositions() {
|
||||
return nodes.map(node => ({
|
||||
id: Number(node.id),
|
||||
position_x: node.position.x,
|
||||
position_y: node.position.y
|
||||
}));
|
||||
}
|
||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
||||
|
||||
function handleSavePositions() {
|
||||
const positions = getPositions();
|
||||
|
@ -135,106 +116,34 @@ export function OssFlow() {
|
|||
});
|
||||
}
|
||||
|
||||
function handleCreateOperation(inputs: number[]) {
|
||||
const positions = getPositions();
|
||||
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
promptCreateOperation({
|
||||
defaultX: target.x,
|
||||
defaultY: target.y,
|
||||
inputs: inputs,
|
||||
positions: positions,
|
||||
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout)
|
||||
function handleCreateOperation() {
|
||||
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateOperation({
|
||||
oss: schema,
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
positions: getPositions(),
|
||||
initialInputs: selected,
|
||||
onCreate: () =>
|
||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteOperation(target: number) {
|
||||
if (!canDelete(target)) {
|
||||
return;
|
||||
}
|
||||
promptDeleteOperation(target, getPositions());
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
handleDeleteOperation(selected[0]);
|
||||
}
|
||||
|
||||
function handleInputCreate(target: number) {
|
||||
const operation = schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
const operation = schema.operationByID.get(selected[0]);
|
||||
if (!operation || !canDelete(operation)) {
|
||||
return;
|
||||
}
|
||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
||||
toast.error(errorMsg.inputAlreadyExists);
|
||||
return;
|
||||
}
|
||||
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() }
|
||||
showDeleteOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
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) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -248,14 +157,6 @@ export function OssFlow() {
|
|||
setHoverOperation(null);
|
||||
}
|
||||
|
||||
function handleContextMenuHide() {
|
||||
setIsContextMenuOpen(false);
|
||||
}
|
||||
|
||||
function handleCanvasClick() {
|
||||
handleContextMenuHide();
|
||||
}
|
||||
|
||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -265,10 +166,7 @@ export function OssFlow() {
|
|||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (!isMutable) {
|
||||
if (isProcessing || !isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
|
@ -280,7 +178,7 @@ export function OssFlow() {
|
|||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleCreateOperation(selected);
|
||||
handleCreateOperation();
|
||||
return;
|
||||
}
|
||||
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'
|
||||
>
|
||||
<ToolbarOssGraph
|
||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||
onCreate={() => handleCreateOperation(selected)}
|
||||
onCreate={handleCreateOperation}
|
||||
onDelete={handleDeleteSelected}
|
||||
onEdit={() => handleEditOperation(selected[0])}
|
||||
onExecute={handleExecuteSelected}
|
||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||
onSavePositions={handleSavePositions}
|
||||
onSaveImage={handleSaveImage}
|
||||
/>
|
||||
</Overlay>
|
||||
{menuProps ? (
|
||||
<NodeContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
onHide={handleContextMenuHide}
|
||||
onDelete={handleDeleteOperation}
|
||||
onCreateInput={handleInputCreate}
|
||||
onEditSchema={handleEditSchema}
|
||||
onEditOperation={handleEditOperation}
|
||||
onExecuteOperation={handleOperationExecute}
|
||||
onRelocateConstituents={handleRelocateConstituents}
|
||||
{...menuProps}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||
|
||||
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
edgesFocusable={false}
|
||||
nodesFocusable={false}
|
||||
fitView
|
||||
|
@ -336,11 +218,13 @@ export function OssFlow() {
|
|||
minZoom={ZOOM_MIN}
|
||||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
||||
onClick={() => setIsContextMenuOpen(false)}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeContextMenu={handleContextMenu}
|
||||
onClick={handleCanvasClick}
|
||||
onNodeDragStart={() => setIsContextMenuOpen(false)}
|
||||
>
|
||||
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
|
||||
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { useReactFlow } from 'reactflow';
|
||||
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 {
|
||||
|
@ -13,13 +17,13 @@ import {
|
|||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconImage,
|
||||
IconLineStraight,
|
||||
IconLineWave,
|
||||
IconNewItem,
|
||||
IconReset,
|
||||
IconSave
|
||||
} from '@/components/Icons';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
|
@ -28,30 +32,21 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
|||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
import { VIEW_PADDING } from './OssFlow';
|
||||
import { useGetPositions } from './useGetPositions';
|
||||
|
||||
interface ToolbarOssGraphProps {
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
onExecute: () => void;
|
||||
onFitView: () => void;
|
||||
onSaveImage: () => void;
|
||||
onSavePositions: () => void;
|
||||
onResetPositions: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarOssGraph({
|
||||
onCreate,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onExecute,
|
||||
onFitView,
|
||||
onSaveImage,
|
||||
onSavePositions,
|
||||
onResetPositions
|
||||
}: ToolbarOssGraphProps) {
|
||||
const { schema, selected, isMutable, canDelete } = useOssEdit();
|
||||
export function ToolbarOssGraph({ onCreate, onDelete, onResetPositions }: ToolbarOssGraphProps) {
|
||||
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { fitView } = useReactFlow();
|
||||
const selectedOperation = schema.operationByID.get(selected[0]);
|
||||
const getPositions = useGetPositions();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
|
@ -60,6 +55,11 @@ export function ToolbarOssGraph({
|
|||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
const { operationExecute } = useOperationExecute();
|
||||
|
||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||
|
||||
const readyForSynthesis = (() => {
|
||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||
return false;
|
||||
|
@ -81,6 +81,44 @@ export function ToolbarOssGraph({
|
|||
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 (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='cc-icons'>
|
||||
|
@ -92,7 +130,7 @@ export function ToolbarOssGraph({
|
|||
<MiniButton
|
||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сбросить вид'
|
||||
onClick={onFitView}
|
||||
onClick={handleFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||
|
@ -127,11 +165,6 @@ export function ToolbarOssGraph({
|
|||
}
|
||||
onClick={toggleEdgeAnimate}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сохранить изображение'
|
||||
onClick={onSaveImage}
|
||||
/>
|
||||
<BadgeHelp
|
||||
topic={HelpTopic.UI_OSS_GRAPH}
|
||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||
|
@ -144,7 +177,7 @@ export function ToolbarOssGraph({
|
|||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
disabled={isProcessing}
|
||||
onClick={onSavePositions}
|
||||
onClick={handleSavePositions}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||
|
@ -156,18 +189,18 @@ export function ToolbarOssGraph({
|
|||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
||||
onClick={onExecute}
|
||||
onClick={handleOperationExecute}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
disabled={selected.length !== 1 || isProcessing}
|
||||
onClick={onEdit}
|
||||
onClick={handleEditOperation}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,23 +3,24 @@
|
|||
import { Overlay } from '@/components/Container';
|
||||
import { IconConsolidation, IconRSForm } from '@/components/Icons';
|
||||
import { Indicator } from '@/components/View';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { globalIDs, PARAMETER } from '@/utils/constants';
|
||||
import { truncateToLastWord } from '@/utils/utils';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { OperationType } from '../../../../backend/types';
|
||||
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 {
|
||||
node: OssNodeInternal;
|
||||
}
|
||||
|
||||
export function NodeCore({ node }: NodeCoreProps) {
|
||||
const setHover = useTooltipsStore(state => state.setActiveOperation);
|
||||
const setHover = useOperationTooltipStore(state => state.setActiveOperation);
|
||||
|
||||
const hasFile = !!node.data.operation.result;
|
||||
const longLabel = node.data.label.length > PARAMETER.ossLongLabel;
|
||||
const labelText = truncateToLastWord(node.data.label, PARAMETER.ossTruncateLabel);
|
||||
const longLabel = node.data.label.length > LONG_LABEL_CHARS;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -53,10 +54,11 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
<div
|
||||
className='h-[34px] w-[144px] flex items-center justify-center'
|
||||
data-tooltip-id={globalIDs.operation_tooltip}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
onMouseEnter={() => setHover(node.data.operation)}
|
||||
>
|
||||
<div
|
||||
className='text-center'
|
||||
className='text-center line-clamp-2'
|
||||
style={{
|
||||
fontSize: longLabel ? '12px' : '14px',
|
||||
lineHeight: longLabel ? '16px' : '20px',
|
||||
|
@ -64,7 +66,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
paddingRight: longLabel ? '10px' : '4px'
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{node.data.label}
|
||||
</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';
|
||||
|
||||
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 {
|
||||
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 { MenuRole } from '@/features/library/components';
|
||||
|
||||
import { MenuEditOss } from './MenuEditOss';
|
||||
import { MenuMain } from './MenuMain';
|
||||
import { useOssEdit } from './OssEditContext';
|
||||
|
||||
export function MenuOssTabs() {
|
||||
const { deleteSchema, promptRelocateConstituents, isMutable, isOwned, schema } = useOssEdit();
|
||||
const router = useConceptNavigation();
|
||||
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, []);
|
||||
}
|
||||
|
||||
const { isOwned, schema } = useOssEdit();
|
||||
const { user } = useAuthSuspense();
|
||||
return (
|
||||
<div className='flex border-r-2'>
|
||||
<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}
|
||||
/>
|
||||
{isMutable ? (
|
||||
<DropdownButton
|
||||
text='Удалить схему'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={isProcessing || role < UserRole.OWNER}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
<MenuMain />
|
||||
|
||||
<Divider margins='mx-3 my-1' />
|
||||
<MenuEditOss />
|
||||
|
||||
{!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(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}
|
||||
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,17 +4,17 @@ import { createContext, useContext, useEffect, useState } from 'react';
|
|||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
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 { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
import { type IOperationPosition, OperationType } from '../../backend/types';
|
||||
import { useOssSuspense } from '../../backend/useOSS';
|
||||
import { type IOperationSchema } from '../../models/oss';
|
||||
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||
|
||||
export enum OssTabID {
|
||||
CARD = 0,
|
||||
|
@ -39,15 +39,9 @@ export interface IOssEditContext {
|
|||
navigateTab: (tab: OssTabID) => void;
|
||||
navigateOperationSchema: (target: number) => void;
|
||||
|
||||
canDeleteOperation: (target: IOperation) => boolean;
|
||||
deleteSchema: () => void;
|
||||
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);
|
||||
|
@ -80,12 +74,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
|
||||
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();
|
||||
|
||||
useEffect(
|
||||
|
@ -104,7 +92,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
id: schema.id,
|
||||
tab: tab
|
||||
});
|
||||
router.push(url);
|
||||
router.push({ path: url });
|
||||
}
|
||||
|
||||
function navigateOperationSchema(target: number) {
|
||||
|
@ -112,86 +100,29 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
if (!node?.result) {
|
||||
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() {
|
||||
if (!window.confirm(promptText.deleteOSS)) {
|
||||
return;
|
||||
}
|
||||
void deleteItem(schema.id).then(() => {
|
||||
if (searchLocation === schema.location) {
|
||||
setSearchLocation('');
|
||||
void deleteItem({
|
||||
target: schema.id,
|
||||
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) {
|
||||
showCreateOperation({
|
||||
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) {
|
||||
function canDeleteOperation(target: IOperation) {
|
||||
if (target.operation_type === OperationType.INPUT) {
|
||||
return true;
|
||||
}
|
||||
return schema.graph.expandOutputs([target]).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 schema.graph.expandOutputs([target.id]).length === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -200,22 +131,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
schema,
|
||||
selected,
|
||||
|
||||
navigateTab,
|
||||
|
||||
deleteSchema,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
|
||||
setSelected,
|
||||
|
||||
navigateTab,
|
||||
navigateOperationSchema,
|
||||
promptCreateOperation,
|
||||
canDelete,
|
||||
promptDeleteOperation,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
promptRelocateConstituents
|
||||
|
||||
canDeleteOperation,
|
||||
deleteSchema,
|
||||
setSelected
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useParams } from 'react-router';
|
|||
import { z } from 'zod';
|
||||
|
||||
import { urls, useBlockNavigation, useConceptNavigation } from '@/app';
|
||||
import { ConstituentaTooltip } from '@/features/rsform/components';
|
||||
|
||||
import { isAxiosError } from '@/backend/apiTransport';
|
||||
import { TextURL } from '@/components/Control';
|
||||
|
@ -13,6 +14,8 @@ import { type ErrorData } from '@/components/InfoError';
|
|||
import { useQueryStrings } from '@/hooks/useQueryStrings';
|
||||
import { useModificationStore } from '@/stores/modification';
|
||||
|
||||
import { OperationTooltip } from '../../components/OperationTooltip';
|
||||
|
||||
import { OssEditState, OssTabID } from './OssEditContext';
|
||||
import { OssTabs } from './OssTabs';
|
||||
|
||||
|
@ -37,12 +40,14 @@ export function OssPage() {
|
|||
useEffect(() => setIsModified(false), [setIsModified]);
|
||||
|
||||
if (!urlData.id) {
|
||||
router.replace(urls.page404);
|
||||
router.replace({ path: urls.page404, force: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ProcessError}>
|
||||
<OperationTooltip />
|
||||
<ConstituentaTooltip />
|
||||
<OssEditState itemID={urlData.id}>
|
||||
<OssTabs activeTab={urlData.tab} />
|
||||
</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 { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
import { KEYS } from '@/backend/configuration';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUpdateTimestamp } from '@/features/library';
|
||||
import { useUpdateTimestamp } from '@/features/library/backend/useUpdateTimestamp';
|
||||
|
||||
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