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 { useNavigate } from 'react-router';
import { contextOutsideScope } from '@/utils/labels';
interface INavigationContext {
push: (path: string, newTab?: boolean) => void;
replace: (path: string) => void;
@ -21,7 +19,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;
};

View File

@ -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} />;
}

View File

@ -13,7 +13,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 +23,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 +98,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 +506,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;

View File

@ -13,7 +13,6 @@ import {
IconRSForm
} from '@/components/Icons';
import { useClickedOutside } from '@/hooks/useClickedOutside';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../backend/types';
@ -21,6 +20,10 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../OssEditContext';
// pixels - size of OSS context menu
const MENU_WIDTH = 200;
const MENU_HEIGHT = 200;
export interface ContextMenuData {
operation: IOperation;
cursorX: number;
@ -116,8 +119,8 @@ export function NodeContextMenu({
<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='Редактировать'

View File

@ -29,6 +29,7 @@ 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 { useOSSGraphStore } from '../../../stores/ossGraph';
import { useOssEdit } from '../OssEditContext';
@ -336,11 +337,11 @@ export function OssFlow() {
minZoom={ZOOM_MIN}
nodesConnectable={false}
snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
snapGrid={[GRID_SIZE, GRID_SIZE]}
onNodeContextMenu={handleContextMenu}
onClick={handleCanvasClick}
>
{showGrid ? <Background gap={PARAMETER.ossGridSize} /> : null}
{showGrid ? <Background gap={GRID_SIZE} /> : null}
</ReactFlow>
</div>
</div>

View File

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

View File

@ -3,6 +3,7 @@
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
@ -21,7 +22,6 @@ import {
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';

View File

@ -49,6 +49,10 @@ import { ViewHidden } from './ViewHidden';
const ZOOM_MAX = 3;
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() {
const mainHeight = useMainHeight();
const flow = useReactFlow();
@ -288,8 +292,7 @@ export function TGFlow() {
function handleNodeEnter(event: React.MouseEvent, cstID: number) {
setHoverID(cstID);
setHoverLeft(
event.clientX / window.innerWidth >= PARAMETER.graphHoverXLimit ||
event.clientY / window.innerHeight >= PARAMETER.graphHoverYLimit
event.clientX / window.innerWidth >= HOVER_LIMIT_X || event.clientY / window.innerHeight >= HOVER_LIMIT_Y
);
}

View File

@ -7,6 +7,7 @@ import { useAuthSuspense } from '@/features/auth';
import { AccessPolicy } from '@/features/library';
import { LocationHead } from '@/features/library/models/library';
import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
@ -39,7 +40,7 @@ import {
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
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 { useDownloadRSForm } from '../../backend/useDownloadRSForm';
@ -384,7 +385,7 @@ export function MenuRSTabs() {
noBorder
noOutline
tabIndex={-1}
title={`Режим ${labelAccessMode(role)}`}
title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={
@ -402,28 +403,28 @@ export function MenuRSTabs() {
/>
<Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton
text={labelAccessMode(UserRole.READER)}
title={describeAccessMode(UserRole.READER)}
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}
icon={<IconReader size='1rem' className='icon-primary' />}
onClick={() => handleChangeMode(UserRole.READER)}
/>
<DropdownButton
text={labelAccessMode(UserRole.EDITOR)}
title={describeAccessMode(UserRole.EDITOR)}
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={() => handleChangeMode(UserRole.EDITOR)}
/>
<DropdownButton
text={labelAccessMode(UserRole.OWNER)}
title={describeAccessMode(UserRole.OWNER)}
text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)}
/>
<DropdownButton
text={labelAccessMode(UserRole.ADMIN)}
title={describeAccessMode(UserRole.ADMIN)}
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user.is_staff}
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
minimalTimeout: 10, // milliseconds delay for fast updates
zoomDuration: 500, // milliseconds animation duration
moveDuration: 500, // milliseconds - duration of move animation
dropdownDuration: 200, // milliseconds - duration of dropdown animation
navigationDuration: 300, // milliseconds navigation duration
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
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
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
graphNodeRadius: 20, // pixels - radius 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',
errorNodeLabel: '[ERROR]',
@ -50,8 +39,7 @@ export const PARAMETER = {
* Numeric limitations.
*/
export const limits = {
location_len: 500,
max_semantic_index: 900
location_len: 500
};
/**
@ -133,12 +121,10 @@ export const prefixes = {
operation_list: 'operation_list_',
csttype_list: 'csttype_',
policy_list: 'policy_list_',
library_filters_list: 'library_filters_list_',
location_head_list: 'location_head_list_',
folders_list: 'folders_list_',
topic_list: 'topic_list_',
topic_item: 'topic_item_',
library_list: 'library_list_',
user_subs: 'user_subs_',
user_editors: 'user_editors_',
wordform_list: 'wordform_list_',

View File

@ -4,37 +4,6 @@
* Label is a short text used to represent an entity.
* 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.
@ -116,8 +85,3 @@ export const promptText = {
ownerChange:
'Вы уверены, что хотите изменить владельца? Вы потеряете право управления данной схемой. Данное действие отменить нельзя'
};
// ============== INTERNAL LABELS FOR DEVELOPERS TEXT ================
export function contextOutsideScope(contextName: string, contextState: string): string {
return `${contextName} has to be used within <${contextState}>`;
}