R: Redistribute constant and global dependencies

This commit is contained in:
Ivan 2025-02-22 18:39:58 +03:00
parent 664fdb585c
commit b29a4e7b49
12 changed files with 87 additions and 89 deletions

View File

@ -3,8 +3,6 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { contextOutsideScope } from '@/utils/labels';
interface INavigationContext { interface INavigationContext {
push: (path: string, newTab?: boolean) => void; push: (path: string, newTab?: boolean) => void;
replace: (path: string) => void; replace: (path: string) => void;
@ -21,7 +19,7 @@ const NavigationContext = createContext<INavigationContext | null>(null);
export const useConceptNavigation = () => { export const useConceptNavigation = () => {
const context = useContext(NavigationContext); const context = useContext(NavigationContext);
if (!context) { if (!context) {
throw new Error(contextOutsideScope('useConceptNavigation', 'NavigationState')); throw new Error('useConceptNavigation has to be used within <NavigationState>');
} }
return context; return context;
}; };

View File

@ -1,8 +1,10 @@
import { type Styling, type Titled } from '@/components/props'; import { type Styling, type Titled } from '@/components/props';
import { PARAMETER } from '@/utils/constants';
import { ValueIcon } from './ValueIcon'; import { ValueIcon } from './ValueIcon';
// characters - threshold for small labels - small font
const SMALL_THRESHOLD = 3;
interface ValueStatsProps extends Styling, Titled { interface ValueStatsProps extends Styling, Titled {
/** Id of the component. */ /** Id of the component. */
id: string; id: string;
@ -18,5 +20,5 @@ interface ValueStatsProps extends Styling, Titled {
* Displays statistics value with an icon. * Displays statistics value with an icon.
*/ */
export function ValueStats(props: ValueStatsProps) { export function ValueStats(props: ValueStatsProps) {
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />; return <ValueIcon dense smallThreshold={SMALL_THRESHOLD} textClassName='min-w-[1.4rem]' {...props} />;
} }

View File

@ -13,7 +13,6 @@ import {
isSetTypification isSetTypification
} from '@/features/rsform/models/rslangAPI'; } from '@/features/rsform/models/rslangAPI';
import { limits, PARAMETER } from '@/utils/constants';
import { infoMsg } from '@/utils/labels'; import { infoMsg } from '@/utils/labels';
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
@ -24,6 +23,13 @@ import { describeSubstitutionError } from '../labels';
import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss'; import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss';
import { type Position2D } from './ossLayout'; import { type Position2D } from './ossLayout';
export const GRID_SIZE = 10; // pixels - size of OSS grid
const MIN_DISTANCE = 20; // pixels - minimum distance between node centers
const DISTANCE_X = 180; // pixels - insert x-distance between node centers
const DISTANCE_Y = 100; // pixels - insert y-distance between node centers
const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution
/** /**
* Checks if a given target {@link IOperation} matches the specified query using. * Checks if a given target {@link IOperation} matches the specified query using.
* *
@ -92,7 +98,7 @@ export class SubstitutionValidator {
this.schemaByCst.set(item.id, schema); this.schemaByCst.set(item.id, schema);
}); });
}); });
let index = limits.max_semantic_index; let index = STARTING_SUB_INDEX;
substitutions.forEach(item => { substitutions.forEach(item => {
this.constituents.add(item.original); this.constituents.add(item.original);
this.constituents.add(item.substitution); this.constituents.add(item.substitution);
@ -500,27 +506,27 @@ export function calculateInsertPosition(
} }
const maxX = Math.max(...inputsNodes.map(node => node.position_x)); const maxX = Math.max(...inputsNodes.map(node => node.position_x));
const minY = Math.min(...inputsNodes.map(node => node.position_y)); const minY = Math.min(...inputsNodes.map(node => node.position_y));
result.x = maxX + PARAMETER.ossDistanceX; result.x = maxX + DISTANCE_X;
result.y = minY; result.y = minY;
} else { } else {
const argNodes = positions.filter(pos => argumentsOps.includes(pos.id)); const argNodes = positions.filter(pos => argumentsOps.includes(pos.id));
const maxY = Math.max(...argNodes.map(node => node.position_y)); const maxY = Math.max(...argNodes.map(node => node.position_y));
const minX = Math.min(...argNodes.map(node => node.position_x)); const minX = Math.min(...argNodes.map(node => node.position_x));
const maxX = Math.max(...argNodes.map(node => node.position_x)); const maxX = Math.max(...argNodes.map(node => node.position_x));
result.x = Math.ceil((maxX + minX) / 2 / PARAMETER.ossGridSize) * PARAMETER.ossGridSize; result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE;
result.y = maxY + PARAMETER.ossDistanceY; result.y = maxY + DISTANCE_Y;
} }
let flagIntersect = false; let flagIntersect = false;
do { do {
flagIntersect = positions.some( flagIntersect = positions.some(
position => position =>
Math.abs(position.position_x - result.x) < PARAMETER.ossMinDistance && Math.abs(position.position_x - result.x) < MIN_DISTANCE &&
Math.abs(position.position_y - result.y) < PARAMETER.ossMinDistance Math.abs(position.position_y - result.y) < MIN_DISTANCE
); );
if (flagIntersect) { if (flagIntersect) {
result.x += PARAMETER.ossMinDistance; result.x += MIN_DISTANCE;
result.y += PARAMETER.ossMinDistance; result.y += MIN_DISTANCE;
} }
} while (flagIntersect); } while (flagIntersect);
return result; return result;

View File

@ -13,7 +13,6 @@ import {
IconRSForm IconRSForm
} from '@/components/Icons'; } from '@/components/Icons';
import { useClickedOutside } from '@/hooks/useClickedOutside'; import { useClickedOutside } from '@/hooks/useClickedOutside';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../backend/types'; import { OperationType } from '../../../backend/types';
@ -21,6 +20,10 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
import { type IOperation } from '../../../models/oss'; import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
// pixels - size of OSS context menu
const MENU_WIDTH = 200;
const MENU_HEIGHT = 200;
export interface ContextMenuData { export interface ContextMenuData {
operation: IOperation; operation: IOperation;
cursorX: number; cursorX: number;
@ -116,8 +119,8 @@ export function NodeContextMenu({
<div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}> <div ref={ref} className='absolute select-none' style={{ top: cursorY, left: cursorX }}>
<Dropdown <Dropdown
isOpen={isOpen} isOpen={isOpen}
stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth} stretchLeft={cursorX >= window.innerWidth - MENU_WIDTH}
stretchTop={cursorY >= window.innerHeight - PARAMETER.ossContextMenuHeight} stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT}
> >
<DropdownButton <DropdownButton
text='Редактировать' text='Редактировать'

View File

@ -29,6 +29,7 @@ import { useInputCreate } from '../../../backend/useInputCreate';
import { useMutatingOss } from '../../../backend/useMutatingOss'; import { useMutatingOss } from '../../../backend/useMutatingOss';
import { useOperationExecute } from '../../../backend/useOperationExecute'; import { useOperationExecute } from '../../../backend/useOperationExecute';
import { useUpdatePositions } from '../../../backend/useUpdatePositions'; import { useUpdatePositions } from '../../../backend/useUpdatePositions';
import { GRID_SIZE } from '../../../models/ossAPI';
import { type OssNode } from '../../../models/ossLayout'; import { type OssNode } from '../../../models/ossLayout';
import { useOSSGraphStore } from '../../../stores/ossGraph'; import { useOSSGraphStore } from '../../../stores/ossGraph';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
@ -336,11 +337,11 @@ export function OssFlow() {
minZoom={ZOOM_MIN} minZoom={ZOOM_MIN}
nodesConnectable={false} nodesConnectable={false}
snapToGrid={true} snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]} snapGrid={[GRID_SIZE, GRID_SIZE]}
onNodeContextMenu={handleContextMenu} onNodeContextMenu={handleContextMenu}
onClick={handleCanvasClick} onClick={handleCanvasClick}
> >
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null} {showGrid ? <Background gap={GRID_SIZE} /> : null}
</ReactFlow> </ReactFlow>
</div> </div>
</div> </div>

View File

@ -4,11 +4,14 @@ import { Overlay } from '@/components/Container';
import { IconConsolidation, IconRSForm } from '@/components/Icons'; import { IconConsolidation, IconRSForm } from '@/components/Icons';
import { Indicator } from '@/components/View'; import { Indicator } from '@/components/View';
import { useTooltipsStore } from '@/stores/tooltips'; import { useTooltipsStore } from '@/stores/tooltips';
import { globalIDs, PARAMETER } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
import { OperationType } from '../../../../backend/types'; import { OperationType } from '../../../../backend/types';
import { type OssNodeInternal } from '../../../../models/ossLayout'; import { type OssNodeInternal } from '../../../../models/ossLayout';
// characters - threshold for long labels - small font
const LONG_LABEL_CHARS = 14;
interface NodeCoreProps { interface NodeCoreProps {
node: OssNodeInternal; node: OssNodeInternal;
} }
@ -17,7 +20,7 @@ export function NodeCore({ node }: NodeCoreProps) {
const setHover = useTooltipsStore(state => state.setActiveOperation); const setHover = useTooltipsStore(state => state.setActiveOperation);
const hasFile = !!node.data.operation.result; const hasFile = !!node.data.operation.result;
const longLabel = node.data.label.length > PARAMETER.ossLongLabel; const longLabel = node.data.label.length > LONG_LABEL_CHARS;
return ( return (
<> <>

View File

@ -3,6 +3,7 @@
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container'; import { Divider } from '@/components/Container';
import { Button } from '@/components/Control'; import { Button } from '@/components/Control';
@ -21,7 +22,6 @@ import {
IconReader, IconReader,
IconShare IconShare
} from '@/components/Icons'; } from '@/components/Icons';
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
import { sharePage } from '@/utils/utils'; import { sharePage } from '@/utils/utils';
import { useMutatingOss } from '../../backend/useMutatingOss'; import { useMutatingOss } from '../../backend/useMutatingOss';

View File

@ -49,6 +49,10 @@ import { ViewHidden } from './ViewHidden';
const ZOOM_MAX = 3; const ZOOM_MAX = 3;
const ZOOM_MIN = 0.25; const ZOOM_MIN = 0.25;
// ratio to client size used to determine which side of screen popup should be
const HOVER_LIMIT_X = 0.4;
const HOVER_LIMIT_Y = 0.6;
export function TGFlow() { export function TGFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const flow = useReactFlow(); const flow = useReactFlow();
@ -288,8 +292,7 @@ export function TGFlow() {
function handleNodeEnter(event: React.MouseEvent, cstID: number) { function handleNodeEnter(event: React.MouseEvent, cstID: number) {
setHoverID(cstID); setHoverID(cstID);
setHoverLeft( setHoverLeft(
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit || event.clientX / window.innerWidth >= HOVER_LIMIT_X || event.clientY / window.innerHeight >= HOVER_LIMIT_Y
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
); );
} }

View File

@ -7,6 +7,7 @@ import { useAuthSuspense } from '@/features/auth';
import { AccessPolicy } from '@/features/library'; import { AccessPolicy } from '@/features/library';
import { LocationHead } from '@/features/library/models/library'; import { LocationHead } from '@/features/library/models/library';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container'; import { Divider } from '@/components/Container';
import { Button } from '@/components/Control'; import { Button } from '@/components/Control';
@ -39,7 +40,7 @@ import {
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { EXTEOR_TRS_FILE } from '@/utils/constants'; import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { describeAccessMode, labelAccessMode, tooltipText } from '@/utils/labels'; import { tooltipText } from '@/utils/labels';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils'; import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { useDownloadRSForm } from '../../backend/useDownloadRSForm'; import { useDownloadRSForm } from '../../backend/useDownloadRSForm';
@ -384,7 +385,7 @@ export function MenuRSTabs() {
noBorder noBorder
noOutline noOutline
tabIndex={-1} tabIndex={-1}
title={`Режим ${labelAccessMode(role)}`} title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen} hideTitle={accessMenu.isOpen}
className='h-full pr-2' className='h-full pr-2'
icon={ icon={
@ -402,28 +403,28 @@ export function MenuRSTabs() {
/> />
<Dropdown isOpen={accessMenu.isOpen}> <Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton <DropdownButton
text={labelAccessMode(UserRole.READER)} text={labelUserRole(UserRole.READER)}
title={describeAccessMode(UserRole.READER)} title={describeUserRole(UserRole.READER)}
icon={<IconReader size='1rem' className='icon-primary' />} icon={<IconReader size='1rem' className='icon-primary' />}
onClick={() => handleChangeMode(UserRole.READER)} onClick={() => handleChangeMode(UserRole.READER)}
/> />
<DropdownButton <DropdownButton
text={labelAccessMode(UserRole.EDITOR)} text={labelUserRole(UserRole.EDITOR)}
title={describeAccessMode(UserRole.EDITOR)} title={describeUserRole(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />} icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))} disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
onClick={() => handleChangeMode(UserRole.EDITOR)} onClick={() => handleChangeMode(UserRole.EDITOR)}
/> />
<DropdownButton <DropdownButton
text={labelAccessMode(UserRole.OWNER)} text={labelUserRole(UserRole.OWNER)}
title={describeAccessMode(UserRole.OWNER)} title={describeUserRole(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />} icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!isOwned} disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)} onClick={() => handleChangeMode(UserRole.OWNER)}
/> />
<DropdownButton <DropdownButton
text={labelAccessMode(UserRole.ADMIN)} text={labelUserRole(UserRole.ADMIN)}
title={describeAccessMode(UserRole.ADMIN)} title={describeUserRole(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />} icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user.is_staff} disabled={!user.is_staff}
onClick={() => handleChangeMode(UserRole.ADMIN)} onClick={() => handleChangeMode(UserRole.ADMIN)}

View File

@ -0,0 +1,31 @@
import { UserRole } from './stores/role';
/**
* Retrieves label for {@link UserRole}.
*/
export function labelUserRole(mode: UserRole): string {
// prettier-ignore
switch (mode) {
case UserRole.READER: return 'Читатель';
case UserRole.EDITOR: return 'Редактор';
case UserRole.OWNER: return 'Владелец';
case UserRole.ADMIN: return 'Администратор';
}
}
/**
* Retrieves description for {@link UserRole}.
*/
export function describeUserRole(mode: UserRole): string {
// prettier-ignore
switch (mode) {
case UserRole.READER:
return 'Режим запрещает редактирование';
case UserRole.EDITOR:
return 'Режим редактирования';
case UserRole.OWNER:
return 'Режим владельца';
case UserRole.ADMIN:
return 'Режим администратора';
}
}

View File

@ -11,33 +11,22 @@ export const PARAMETER = {
refreshTimeout: 100, // milliseconds delay for post-refresh actions refreshTimeout: 100, // milliseconds delay for post-refresh actions
minimalTimeout: 10, // milliseconds delay for fast updates minimalTimeout: 10, // milliseconds delay for fast updates
zoomDuration: 500, // milliseconds animation duration zoomDuration: 500, // milliseconds animation duration
moveDuration: 500, // milliseconds - duration of move animation
dropdownDuration: 200, // milliseconds - duration of dropdown animation
navigationDuration: 300, // milliseconds navigation duration navigationDuration: 300, // milliseconds navigation duration
navigationPopupDelay: 300, // milliseconds delay for navigation popup navigationPopupDelay: 300, // milliseconds delay for navigation popup
graphPopupDelay: 500, // milliseconds delay for graph popup selections
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
fastAnimation: 200, // milliseconds - duration of fast animation fastAnimation: 200, // milliseconds - duration of fast animation
fadeDuration: 300, // milliseconds - duration of fade animation fadeDuration: 300, // milliseconds - duration of fade animation
dropdownDuration: 200, // milliseconds - duration of dropdown animation
moveDuration: 500, // milliseconds - duration of move animation
ossImageWidth: 1280, // pixels - size of OSS image ossImageWidth: 1280, // pixels - size of OSS image
ossImageHeight: 960, // pixels - size of OSS image ossImageHeight: 960, // pixels - size of OSS image
ossContextMenuWidth: 200, // pixels - width of OSS context menu
ossContextMenuHeight: 200, // pixels - height of OSS context menu
ossGridSize: 10, // pixels - size of OSS grid
ossMinDistance: 20, // pixels - minimum distance between node centers
ossDistanceX: 180, // pixels - insert x-distance between node centers
ossDistanceY: 100, // pixels - insert y-distance between node centers
graphHandleSize: 3, // pixels - size of graph connection handle graphHandleSize: 3, // pixels - size of graph connection handle
graphNodeRadius: 20, // pixels - radius of graph node graphNodeRadius: 20, // pixels - radius of graph node
graphNodePadding: 5, // pixels - padding of graph node graphNodePadding: 5, // pixels - padding of graph node
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
graphPopupDelay: 500, // milliseconds delay for graph popup selections
graphRefreshDelay: 10, // milliseconds delay for graph viewpoint reset
ossLongLabel: 14, // characters - threshold for long labels - small font
statSmallThreshold: 3, // characters - threshold for small labels - small font
logicLabel: 'LOGIC', logicLabel: 'LOGIC',
errorNodeLabel: '[ERROR]', errorNodeLabel: '[ERROR]',
@ -50,8 +39,7 @@ export const PARAMETER = {
* Numeric limitations. * Numeric limitations.
*/ */
export const limits = { export const limits = {
location_len: 500, location_len: 500
max_semantic_index: 900
}; };
/** /**
@ -133,12 +121,10 @@ export const prefixes = {
operation_list: 'operation_list_', operation_list: 'operation_list_',
csttype_list: 'csttype_', csttype_list: 'csttype_',
policy_list: 'policy_list_', policy_list: 'policy_list_',
library_filters_list: 'library_filters_list_',
location_head_list: 'location_head_list_', location_head_list: 'location_head_list_',
folders_list: 'folders_list_', folders_list: 'folders_list_',
topic_list: 'topic_list_', topic_list: 'topic_list_',
topic_item: 'topic_item_', topic_item: 'topic_item_',
library_list: 'library_list_',
user_subs: 'user_subs_', user_subs: 'user_subs_',
user_editors: 'user_editors_', user_editors: 'user_editors_',
wordform_list: 'wordform_list_', wordform_list: 'wordform_list_',

View File

@ -4,37 +4,6 @@
* Label is a short text used to represent an entity. * Label is a short text used to represent an entity.
* Description is a long description used in tooltips. * Description is a long description used in tooltips.
*/ */
import { UserRole } from '@/features/users/stores/role';
/**
* Retrieves label for {@link UserRole}.
*/
export function labelAccessMode(mode: UserRole): string {
// prettier-ignore
switch (mode) {
case UserRole.READER: return 'Читатель';
case UserRole.EDITOR: return 'Редактор';
case UserRole.OWNER: return 'Владелец';
case UserRole.ADMIN: return 'Администратор';
}
}
/**
* Retrieves description for {@link UserRole}.
*/
export function describeAccessMode(mode: UserRole): string {
// prettier-ignore
switch (mode) {
case UserRole.READER:
return 'Режим запрещает редактирование';
case UserRole.EDITOR:
return 'Режим редактирования';
case UserRole.OWNER:
return 'Режим владельца';
case UserRole.ADMIN:
return 'Режим администратора';
}
}
/** /**
* UI info descriptors. * UI info descriptors.
@ -116,8 +85,3 @@ export const promptText = {
ownerChange: ownerChange:
'Вы уверены, что хотите изменить владельца? Вы потеряете право управления данной схемой. Данное действие отменить нельзя' 'Вы уверены, что хотите изменить владельца? Вы потеряете право управления данной схемой. Данное действие отменить нельзя'
}; };
// ============== INTERNAL LABELS FOR DEVELOPERS TEXT ================
export function contextOutsideScope(contextName: string, contextState: string): string {
return `${contextName} has to be used within <${contextState}>`;
}