Compare commits

...

9 Commits

Author SHA1 Message Date
Ivan
9e9d8a3f08 M: Small UI fix
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-10-30 21:48:07 +03:00
Ivan
fdc432ed57 R: Improve conditional hook performance 2024-10-30 21:41:38 +03:00
Ivan
6be49f2752 M: Fix TSDoc text 2024-10-30 21:35:43 +03:00
Ivan
bc460c7f19 R: Improve OSS rendering 2024-10-30 19:56:31 +03:00
Ivan
4462a95885 M: Update manuals 2024-10-30 10:39:05 +03:00
Ivan
dfad742419 Update TODO.txt 2024-10-30 10:36:25 +03:00
Ivan
9842681e51 M: Improve UI borders and styling props 2024-10-29 12:44:51 +03:00
Ivan
dde592bd32 F: Improve Help UI for dialogs 2024-10-29 12:05:23 +03:00
Ivan
227c0a2975 F: Constituenta relocation: final part 2024-10-28 23:55:12 +03:00
63 changed files with 362 additions and 219 deletions

View File

@ -4,9 +4,6 @@ For more specific TODOs see comments in code
[Bugs - PENDING]
- Tab index still selecting background elements when modal is active
[Functionality - PROGRESS]
- OSS change propagation: Advanced features
[Functionality - PENDING]
- Landing page
- Design first user experience
@ -28,7 +25,7 @@ For more specific TODOs see comments in code
- replace reagraph with react-flow in TermGraph and FormulaGraph
- Search functionality for Help Manuals
- Export PDF (Items list, Graph)
- Export PDF (Items list, Graph) - use google search integration filtered by site?
- ARIA (accessibility considerations) - for now machine reading not supported
- Internationalization - at least english version. Consider react.intl
- Sitemap for better SEO and crawler optimization
@ -44,7 +41,6 @@ For more specific TODOs see comments in code
[Tech]
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
- add debounce to some search fields. Consider pagination and dynamic loading
- DataTable: fixed percentage columns, especially for SubstituteTable. Rework column sizing mechanics
- move autopep8 and isort settings from vscode settings to pyproject.toml
- Test UI for #enable-force-dark Chrome setting

View File

@ -16,4 +16,4 @@ djangorestframework-stubs==3.15.1
django-extensions==3.2.3
mypy==1.11.2
pylint==3.3.1
coverage==7.6.3
coverage==7.6.4

View File

@ -1,14 +1,14 @@
tzdata==2024.1
Django==5.1.1
tzdata==2024.2
Django==5.1.2
djangorestframework==3.15.2
django-cors-headers==4.4.0
django-cors-headers==4.5.0
django-filter==24.3
drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.7.1
coreapi==2.3.3
django-rest-passwordreset==1.4.1
django-rest-passwordreset==1.4.2
cctext==0.1.4
pyconcept==0.1.10
pyconcept==0.1.11
psycopg2-binary==2.9.9
psycopg2-binary==2.9.10
gunicorn==23.0.0

View File

@ -20,6 +20,8 @@ import {
IconGraphInputs,
IconGraphOutputs,
IconHide,
IconMoveDown,
IconMoveUp,
IconOSS,
IconPrivate,
IconProps,
@ -159,3 +161,11 @@ export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps
return <IconCstTheorem size={size} className={className ?? 'clr-text-red'} />;
}
}
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) {
return <IconMoveUp size={size} className={className ?? 'clr-text-primary'} />;
} else {
return <IconMoveDown size={size} className={className ?? 'clr-text-primary'} />;
}
}

View File

@ -40,7 +40,7 @@ export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu';
export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu';
export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { LuLightbulb as IconHelp } from 'react-icons/lu';
export { TbHelpOctagon as IconHelp } from 'react-icons/tb';
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';

View File

@ -1,5 +1,3 @@
import clsx from 'clsx';
import TextURL from '@/components/ui/TextURL';
import Tooltip, { PlacesType } from '@/components/ui/Tooltip';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
@ -16,14 +14,14 @@ interface BadgeHelpProps extends CProps.Styling {
place?: PlacesType;
}
function BadgeHelp({ topic, padding, ...restProps }: BadgeHelpProps) {
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
const { showHelp } = useConceptOptions();
if (!showHelp) {
return null;
}
return (
<div tabIndex={-1} id={`help-${topic}`} className={clsx('p-1', padding)}>
<div tabIndex={-1} id={`help-${topic}`} className={padding}>
<IconHelp size='1.25rem' className='icon-primary' />
<Tooltip clickable anchorSelect={`#help-${topic}`} layer='z-modalTooltip' {...restProps}>
<div className='relative' onClick={event => event.stopPropagation()}>

View File

@ -8,13 +8,15 @@ import { CProps } from '../props';
interface InfoUsersProps extends CProps.Styling {
items: UserID[];
prefix: string;
header?: string;
}
function InfoUsers({ items, className, prefix, ...restProps }: InfoUsersProps) {
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
const { getUserLabel } = useUsers();
return (
<div className={clsx('flex flex-col dense', className)} {...restProps}>
{header ? <h2>{header}</h2> : null}
{items.map((user, index) => (
<div key={`${prefix}${index}`}>{getUserLabel(user)}</div>
))}

View File

@ -1,5 +1,7 @@
'use client';
import clsx from 'clsx';
import { IconOSS } from '@/components/Icons';
import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown';
@ -10,12 +12,12 @@ import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemReference } from '@/models/library';
import { prefixes } from '@/utils/constants';
interface MiniSelectorOSSProps {
interface MiniSelectorOSSProps extends CProps.Styling {
items: ILibraryItemReference[];
onSelect: (event: CProps.EventMouse, newValue: ILibraryItemReference) => void;
}
function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) {
function MiniSelectorOSS({ items, onSelect, className, ...restProps }: MiniSelectorOSSProps) {
const ossMenu = useDropdown();
function onToggle(event: CProps.EventMouse) {
@ -27,7 +29,7 @@ function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) {
}
return (
<div ref={ossMenu.ref} className='flex items-center'>
<div ref={ossMenu.ref} className={clsx('flex items-center', className)} {...restProps}>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
title='Операционные схемы'

View File

@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
@ -12,9 +13,10 @@ import { prefixes } from '@/utils/constants';
import { describeConstituenta } from '@/utils/labels';
import BadgeConstituenta from '../info/BadgeConstituenta';
import { CProps } from '../props';
import NoData from '../ui/NoData';
interface PickConstituentaProps {
interface PickConstituentaProps extends CProps.Styling {
id?: string;
prefixID: string;
data?: IConstituenta[];
@ -41,7 +43,9 @@ function PickConstituenta({
describeFunc = describeConstituenta,
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
onBeginFilter,
onSelectValue
onSelectValue,
className,
...restProps
}: PickConstituentaProps) {
const { colors } = useConceptOptions();
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
@ -89,10 +93,10 @@ function PickConstituenta({
);
return (
<div className='border divide-y'>
<div className={clsx('border divide-y', className)} {...restProps}>
<SearchBar
id={id ? `${id}__search` : undefined}
className='clr-input'
className='clr-input rounded-t-md'
noBorder
value={filterText}
onChange={newValue => setFilterText(newValue)}

View File

@ -12,17 +12,19 @@ import { isBasicConcept, matchConstituenta } from '@/models/rsformAPI';
import { describeConstituenta } from '@/utils/labels';
import BadgeConstituenta from '../info/BadgeConstituenta';
import { CProps } from '../props';
import NoData from '../ui/NoData';
import SearchBar from '../ui/SearchBar';
import ToolbarGraphSelection from './ToolbarGraphSelection';
interface PickMultiConstituentaProps {
interface PickMultiConstituentaProps extends CProps.Styling {
id?: string;
schema: IRSForm;
data: IConstituenta[];
prefixID: string;
rows?: number;
noBorder?: boolean;
selected: ConstituentaID[];
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
@ -36,8 +38,11 @@ function PickMultiConstituenta({
data,
prefixID,
rows,
noBorder,
selected,
setSelected
setSelected,
className,
...restProps
}: PickMultiConstituentaProps) {
const { colors } = useConceptOptions();
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@ -118,10 +123,10 @@ function PickMultiConstituenta({
);
return (
<div>
<div className='flex justify-between items-center clr-input px-3 border-x border-t rounded-t-md'>
<div className={clsx(noBorder ? '' : 'border', className)} {...restProps}>
<div className={clsx('px-3 flex justify-between items-center', 'clr-input', 'border-b', 'rounded-t-md')}>
<div className='w-[24ch] select-none whitespace-nowrap'>
Выбраны {selected.length} из {data.length}
{data.length > 0 ? `Выбраны ${selected.length} из ${data.length}` : 'Конституенты'}
</div>
<SearchBar
id='dlg_constituents_search'
@ -145,7 +150,7 @@ function PickMultiConstituenta({
noFooter
rows={rows}
contentHeight='1.3rem'
className={clsx('cc-scroll-y', 'border', 'text-sm', 'select-none')}
className='cc-scroll-y text-sm select-none rounded-b-md'
data={filtered}
columns={columns}
headPosition='0rem'

View File

@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
import { IconMoveDown, IconMoveUp, IconRemove } from '@/components/Icons';
@ -9,7 +10,9 @@ import MiniButton from '@/components/ui/MiniButton';
import NoData from '@/components/ui/NoData';
import { IOperation, OperationID } from '@/models/oss';
interface PickMultiOperationProps {
import { CProps } from '../props';
interface PickMultiOperationProps extends CProps.Styling {
rows?: number;
items: IOperation[];
@ -19,7 +22,7 @@ interface PickMultiOperationProps {
const columnHelper = createColumnHelper<IOperation>();
function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOperationProps) {
function PickMultiOperation({ rows, items, selected, setSelected, className, ...restProps }: PickMultiOperationProps) {
const selectedItems = useMemo(
() => selected.map(itemID => items.find(item => item.id === itemID)!),
[items, selected]
@ -124,7 +127,10 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
);
return (
<div className='flex flex-col gap-1 border-t border-x rounded-t-md clr-input'>
<div
className={clsx('flex flex-col gap-1', ' border-t border-x rounded-md', 'clr-input', className)}
{...restProps}
>
<SelectOperation
noBorder
items={nonSelectedItems} // prettier: split-line
@ -136,7 +142,7 @@ function PickMultiOperation({ rows, items, selected, setSelected }: PickMultiOpe
noFooter
rows={rows}
contentHeight='1.3rem'
className='cc-scroll-y text-sm select-none border-y'
className='cc-scroll-y text-sm select-none border-y rounded-b-md'
data={selectedItems}
columns={columns}
headPosition='0rem'

View File

@ -1,3 +1,4 @@
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
@ -17,7 +18,7 @@ import FlexColumn from '../ui/FlexColumn';
import MiniButton from '../ui/MiniButton';
import SelectLocation from './SelectLocation';
interface PickSchemaProps {
interface PickSchemaProps extends CProps.Styling {
id?: string;
initialFilter?: string;
rows?: number;
@ -39,7 +40,9 @@ function PickSchema({
itemType,
value,
onSelectValue,
baseFilter
baseFilter,
className,
...restProps
}: PickSchemaProps) {
const intl = useIntl();
const { colors } = useConceptOptions();
@ -120,11 +123,11 @@ function PickSchema({
);
return (
<div className='border divide-y'>
<div className='flex justify-between clr-input items-center pr-1'>
<div className={clsx('border divide-y', className)} {...restProps}>
<div className='flex justify-between clr-input items-center pr-1 rounded-t-md'>
<SearchBar
id={id ? `${id}__search` : undefined}
className='clr-input flex-grow'
className='clr-input flex-grow rounded-t-md'
noBorder
value={filterText}
onChange={newValue => setFilterText(newValue)}

View File

@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
@ -14,10 +15,11 @@ import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { errors } from '@/utils/labels';
import { IconAccept, IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons';
import { CProps } from '../props';
import NoData from '../ui/NoData';
import SelectLibraryItem from './SelectLibraryItem';
interface PickSubstitutionsProps {
interface PickSubstitutionsProps extends CProps.Styling {
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
suggestions?: ICstSubstitute[];
@ -40,7 +42,9 @@ function PickSubstitutions({
rows,
schemas,
filter,
allowSelfSubstitution
allowSelfSubstitution,
className,
...restProps
}: PickSubstitutionsProps) {
const { colors } = useConceptOptions();
@ -257,9 +261,9 @@ function PickSubstitutions({
);
return (
<div className='flex flex-col'>
<div className={clsx('flex flex-col', className)} {...restProps}>
<div className='flex items-end gap-3 justify-stretch'>
<div className='flex-grow flex flex-col basis-1/2 gap-[0.125rem] border-x border-t clr-input'>
<div className='flex-grow flex flex-col basis-1/2 gap-[0.125rem] border-x border-t clr-input rounded-t-md'>
<SelectLibraryItem
noBorder
placeholder='Выберите аргумент'
@ -297,7 +301,7 @@ function PickSubstitutions({
/>
</div>
<div className='flex-grow basis-1/2 flex flex-col gap-[0.125rem] border-x border-t clr-input'>
<div className='flex-grow basis-1/2 flex flex-col gap-[0.125rem] border-x border-t clr-input rounded-t-md'>
<SelectLibraryItem
noBorder
placeholder='Выберите аргумент'
@ -320,7 +324,7 @@ function PickSubstitutions({
dense
noHeader
noFooter
className='text-sm border select-none cc-scroll-y'
className='text-sm border rounded-t-none select-none cc-scroll-y'
rows={rows}
contentHeight='1.3rem'
data={substitutionData}

View File

@ -9,17 +9,18 @@ import { prefixes } from '@/utils/constants';
import { describeAccessPolicy, labelAccessPolicy } from '@/utils/labels';
import { PolicyIcon } from '../DomainIcons';
import { CProps } from '../props';
import DropdownButton from '../ui/DropdownButton';
import MiniButton from '../ui/MiniButton';
interface SelectAccessPolicyProps {
interface SelectAccessPolicyProps extends CProps.Styling {
value: AccessPolicy;
onChange: (value: AccessPolicy) => void;
disabled?: boolean;
stretchLeft?: boolean;
}
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange }: SelectAccessPolicyProps) {
function SelectAccessPolicy({ value, disabled, stretchLeft, onChange, ...restProps }: SelectAccessPolicyProps) {
const menu = useDropdown();
const handleChange = useCallback(
@ -33,7 +34,7 @@ function SelectAccessPolicy({ value, disabled, stretchLeft, onChange }: SelectAc
);
return (
<div ref={menu.ref}>
<div ref={menu.ref} {...restProps}>
<MiniButton
title={`Доступ: ${labelAccessPolicy(value)}`}
hideTitle={menu.isOpen}

View File

@ -11,15 +11,16 @@ import { prefixes } from '@/utils/constants';
import { describeCstSource, labelCstSource } from '@/utils/labels';
import { DependencyIcon } from '../DomainIcons';
import { CProps } from '../props';
import DropdownButton from '../ui/DropdownButton';
interface SelectGraphFilterProps {
interface SelectGraphFilterProps extends CProps.Styling {
value: DependencyMode;
dense?: boolean;
onChange: (value: DependencyMode) => void;
}
function SelectGraphFilter({ value, dense, onChange }: SelectGraphFilterProps) {
function SelectGraphFilter({ value, dense, onChange, ...restProps }: SelectGraphFilterProps) {
const menu = useDropdown();
const size = useWindowSize();
@ -32,7 +33,7 @@ function SelectGraphFilter({ value, dense, onChange }: SelectGraphFilterProps) {
);
return (
<div ref={menu.ref}>
<div ref={menu.ref} {...restProps}>
<SelectorButton
transparent
tabIndex={-1}

View File

@ -9,17 +9,18 @@ import { prefixes } from '@/utils/constants';
import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels';
import { ItemTypeIcon } from '../DomainIcons';
import { CProps } from '../props';
import DropdownButton from '../ui/DropdownButton';
import SelectorButton from '../ui/SelectorButton';
interface SelectItemTypeProps {
interface SelectItemTypeProps extends CProps.Styling {
value: LibraryItemType;
onChange: (value: LibraryItemType) => void;
disabled?: boolean;
stretchLeft?: boolean;
}
function SelectItemType({ value, disabled, stretchLeft, onChange }: SelectItemTypeProps) {
function SelectItemType({ value, disabled, stretchLeft, onChange, ...restProps }: SelectItemTypeProps) {
const menu = useDropdown();
const handleChange = useCallback(
@ -33,7 +34,7 @@ function SelectItemType({ value, disabled, stretchLeft, onChange }: SelectItemTy
);
return (
<div ref={menu.ref}>
<div ref={menu.ref} {...restProps}>
<SelectorButton
transparent
title={describeLibraryItemType(value)}

View File

@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import { useCallback } from 'react';
import Dropdown from '@/components/ui/Dropdown';
@ -10,15 +11,16 @@ import { prefixes } from '@/utils/constants';
import { describeLocationHead, labelLocationHead } from '@/utils/labels';
import { LocationIcon } from '../DomainIcons';
import { CProps } from '../props';
import DropdownButton from '../ui/DropdownButton';
interface SelectLocationHeadProps {
interface SelectLocationHeadProps extends CProps.Styling {
value: LocationHead;
onChange: (newValue: LocationHead) => void;
excluded?: LocationHead[];
}
function SelectLocationHead({ value, excluded = [], onChange }: SelectLocationHeadProps) {
function SelectLocationHead({ value, excluded = [], onChange, className, ...restProps }: SelectLocationHeadProps) {
const menu = useDropdown();
const handleChange = useCallback(
@ -30,7 +32,7 @@ function SelectLocationHead({ value, excluded = [], onChange }: SelectLocationHe
);
return (
<div ref={menu.ref} className='h-full text-right'>
<div ref={menu.ref} className={clsx('h-full text-right', className)} {...restProps}>
<SelectorButton
transparent
tabIndex={-1}

View File

@ -11,15 +11,16 @@ import { prefixes } from '@/utils/constants';
import { describeCstMatchMode, labelCstMatchMode } from '@/utils/labels';
import { MatchModeIcon } from '../DomainIcons';
import { CProps } from '../props';
import DropdownButton from '../ui/DropdownButton';
interface SelectMatchModeProps {
interface SelectMatchModeProps extends CProps.Styling {
value: CstMatchMode;
dense?: boolean;
onChange: (value: CstMatchMode) => void;
}
function SelectMatchMode({ value, dense, onChange }: SelectMatchModeProps) {
function SelectMatchMode({ value, dense, onChange, ...restProps }: SelectMatchModeProps) {
const menu = useDropdown();
const size = useWindowSize();
@ -32,7 +33,7 @@ function SelectMatchMode({ value, dense, onChange }: SelectMatchModeProps) {
);
return (
<div ref={menu.ref}>
<div ref={menu.ref} {...restProps}>
<SelectorButton
transparent
titleHtml='Настройка фильтрации <br/>по проверяемым атрибутам'

View File

@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import { useCallback } from 'react';
import { Grammeme } from '@/models/language';
@ -7,13 +8,14 @@ import { prefixes } from '@/utils/constants';
import { DefaultWordForms, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
import WordformButton from '../../dialogs/DlgEditReference/WordformButton';
import { CProps } from '../props';
interface SelectWordFormProps {
interface SelectWordFormProps extends CProps.Styling {
selected: IGrammemeOption[];
setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
}
function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
function SelectWordForm({ selected, setSelected, className, ...restProps }: SelectWordFormProps) {
const handleSelect = useCallback(
(grams: Grammeme[]) => {
setSelected(SelectorGrammemes.filter(({ value }) => grams.includes(value as Grammeme)));
@ -22,7 +24,7 @@ function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
);
return (
<div className='text-xs sm:text-sm'>
<div className={clsx('text-xs sm:text-sm', className)} {...restProps}>
{DefaultWordForms.slice(0, 12).map((data, index) => (
<WordformButton
key={`${prefixes.wordform_list}${index}`}

View File

@ -19,7 +19,7 @@ interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
}
/**
* Button component that provides a customizable `button` with text, icon, tooltips and various styles.
* Customizable `button` with text, icon, tooltips and various styles.
*/
function Button({
icon,

View File

@ -21,7 +21,7 @@ export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'>
}
/**
* Checkbox component that allows users to toggle a boolean value.
* Component that allows toggling a boolean value.
*/
function Checkbox({
disabled,

View File

@ -16,7 +16,7 @@ export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'se
}
/**
* CheckboxTristate component that allows toggling among three states: `true`, `false`, and `null`.
* Component that allows toggling among three states: `true`, `false`, and `null`.
*/
function CheckboxTristate({
disabled,

View File

@ -11,7 +11,7 @@ export interface DividerProps extends CProps.Styling {
}
/**
* Divider component that renders a horizontal or vertical divider with customizable margins and styling.
* Horizontal or vertical divider with customizable margins and styling.
*/
function Divider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
return (

View File

@ -17,7 +17,7 @@ interface DropdownProps extends CProps.Styling {
}
/**
* Dropdown animated component that displays a list of children with optional positioning and visibility control.
* Animated list of children with optional positioning and visibility control.
*/
function Dropdown({
isOpen,

View File

@ -18,7 +18,7 @@ interface DropdownButtonProps extends CProps.AnimatedButton {
}
/**
* DropdownButton animated component that renders a `button` with optional text, icon, and click functionality.
* Animated `button` with optional text, icon, and click functionality.
* It supports optional children for custom content or the default text/icon display.
*/
function DropdownButton({

View File

@ -5,7 +5,7 @@ import { animateDropdownItem } from '@/styling/animations';
import Checkbox, { CheckboxProps } from './Checkbox';
/** DropdownCheckbox animated component that renders a {@link Checkbox} inside a {@link Dropdown} item. */
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
function DropdownCheckbox({ setValue, disabled, ...restProps }: CheckboxProps) {
return (
<motion.div

View File

@ -6,7 +6,7 @@ import { animateDropdownItem } from '@/styling/animations';
import { DividerProps } from './Divider';
/**
* DropdownDivider component that renders {@link Divider} with animation inside {@link Dropdown}.
* {@link Divider} with animation inside {@link Dropdown}.
*/
function DropdownDivider({ vertical, margins = 'mx-2', className, ...restProps }: DividerProps) {
return (

View File

@ -10,7 +10,7 @@ interface EmbedYoutubeProps {
}
/**
* EmbedYoutube component that embeds a YouTube video into the page using the given video ID and dimensions.
* Embeds a YouTube video into the page using the given video ID and dimensions.
*/
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
if (!pxWidth) {

View File

@ -20,7 +20,7 @@ interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
}
/**
* FileInput component for selecting a `file`, displaying the selected file name.
* FileInput is a component for selecting a `file`, displaying the selected file name.
*/
function FileInput({ id, label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null);

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import { CProps } from '../props';
/**
* FlexColumn component that renders a `flex` column container.
* `flex` column container.
* This component is useful for creating vertical layouts with flexbox.
*/
function FlexColumn({ className, children, ...restProps }: CProps.Div) {

View File

@ -1,4 +1,4 @@
// Reexporting reagraph types to wrap in 'use client'.
// Reexporting necessary reagraph types.
'use client';
import { GraphCanvas as GraphUI } from 'reagraph';

View File

@ -13,7 +13,7 @@ interface IndicatorProps extends CProps.Titled, CProps.Styling {
}
/**
* Indicator component that displays a status `icon` with a tooltip.
* Displays a status `icon` with a tooltip.
*/
function Indicator({ icon, title, titleHtml, hideTitle, noPadding, className, ...restProps }: IndicatorProps) {
return (

View File

@ -3,9 +3,15 @@ import clsx from 'clsx';
import { CProps } from '../props';
interface LabelProps extends CProps.Label {
/** Text to display. */
text?: string;
}
/**
* Displays a label with optional text.
*
* Note: Html label component is used only if `htmlFor` prop is set.
*/
function Label({ text, className, ...restProps }: LabelProps) {
if (!text) {
return null;

View File

@ -5,10 +5,13 @@ import { motion } from 'framer-motion';
import { useRef } from 'react';
import useEscapeKey from '@/hooks/useEscapeKey';
import { HelpTopic } from '@/models/miscellaneous';
import { animateModal } from '@/styling/animations';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels';
import { IconClose } from '../Icons';
import BadgeHelp from '../info/BadgeHelp';
import { CProps } from '../props';
import Button from './Button';
import MiniButton from './MiniButton';
@ -27,21 +30,30 @@ export interface ModalProps extends CProps.Styling {
beforeSubmit?: () => boolean;
onSubmit?: () => void;
onCancel?: () => void;
helpTopic?: HelpTopic;
hideHelpWhen?: () => boolean;
}
function Modal({
children,
header,
submitText = 'Продолжить',
submitInvalidTooltip,
readonly,
canSubmit,
overflowVisible,
hideWindow,
beforeSubmit,
onSubmit,
readonly,
onCancel,
canSubmit,
submitInvalidTooltip,
className,
children,
overflowVisible,
submitText = 'Продолжить',
helpTopic,
hideHelpWhen,
...restProps
}: React.PropsWithChildren<ModalProps>) {
const ref = useRef(null);
@ -61,7 +73,7 @@ function Modal({
};
return (
<div className='fixed top-0 left-0 w-full h-full z-modal'>
<div className='fixed top-0 left-0 w-full h-full z-modal cursor-default'>
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-blur')} />
<div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'cc-modal-backdrop')} />
<motion.div
@ -69,7 +81,7 @@ function Modal({
className={clsx(
'z-modal',
'absolute bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border shadow-md',
'border rounded-xl',
'clr-app'
)}
initial={{ ...animateModal.initial }}
@ -85,6 +97,11 @@ function Modal({
onClick={handleCancel}
/>
</Overlay>
{helpTopic && !hideHelpWhen?.() ? (
<Overlay position='left-2 top-2'>
<BadgeHelp topic={helpTopic} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} padding='p-0' />
</Overlay>
) : null}
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}

View File

@ -4,9 +4,7 @@ import clsx from 'clsx';
import { useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal, { ModalProps } from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext';
@ -15,7 +13,6 @@ import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform';
import { generateAlias, validateNewAlias } from '@/models/rsformAPI';
import { inferTemplatedType, substituteTemplateArgs } from '@/models/rslangAPI';
import { PARAMETER } from '@/utils/constants';
import { prompts } from '@/utils/labels';
import FormCreateCst from '../DlgCreateCst/FormCreateCst';
@ -165,14 +162,8 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
canSubmit={validated}
beforeSubmit={handlePrompt}
onSubmit={handleSubmit}
helpTopic={HelpTopic.RSL_TEMPLATES}
>
<Overlay position='top-0 right-[5.9rem]'>
<BadgeHelp
topic={HelpTopic.RSL_TEMPLATES}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
offset={12}
/>
</Overlay>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col'

View File

@ -118,8 +118,10 @@ function TabTemplate({ state, partialUpdate, templateSchema }: TabTemplateProps)
data={filteredData}
onSelectValue={cst => partialUpdate({ prototype: cst })}
prefixID={prefixes.cst_template_ist}
className='rounded-t-none'
rows={8}
/>
<TextArea
id='dlg_template_term'
disabled

View File

@ -4,15 +4,12 @@ import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous';
import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss';
import { PARAMETER } from '@/utils/constants';
import { describeOperationType, labelOperationType } from '@/utils/labels';
import TabInputOperation from './TabInputOperation';
@ -148,11 +145,8 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
canSubmit={isValid}
onSubmit={handleSubmit}
className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.CC_OSS}
>
<Overlay position='top-0 right-0'>
<BadgeHelp topic={HelpTopic.CC_OSS} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} offset={14} />
</Overlay>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col pt-2'

View File

@ -3,14 +3,11 @@
import clsx from 'clsx';
import { useState } from 'react';
import BadgeHelp from '@/components/info/BadgeHelp';
import Checkbox from '@/components/ui/Checkbox';
import Modal, { ModalProps } from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TextInput from '@/components/ui/TextInput';
import { HelpTopic } from '@/models/miscellaneous';
import { IOperation } from '@/models/oss';
import { PARAMETER } from '@/utils/constants';
interface DlgDeleteOperationProps extends Pick<ModalProps, 'hideWindow'> {
target: IOperation;
@ -34,15 +31,8 @@ function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperation
canSubmit={true}
onSubmit={handleSubmit}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column', 'select-none')}
helpTopic={HelpTopic.CC_PROPAGATION}
>
<Overlay position='top-[-2rem] right-[4rem]'>
<BadgeHelp
topic={HelpTopic.CC_PROPAGATION}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
offset={14}
/>
</Overlay>
<TextInput disabled dense noBorder id='operation_alias' label='Операция' value={target.alias} />
<Checkbox
label='Сохранить наследованные конституенты'

View File

@ -4,9 +4,7 @@ import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import useRSFormCache from '@/hooks/useRSFormCache';
import { HelpTopic } from '@/models/miscellaneous';
@ -19,7 +17,6 @@ import {
OperationType
} from '@/models/oss';
import { SubstitutionValidator } from '@/models/ossAPI';
import { PARAMETER } from '@/utils/constants';
import TabArguments from './TabArguments';
import TabOperation from './TabOperation';
@ -195,11 +192,9 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
canSubmit={canSubmit}
onSubmit={handleSubmit}
className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
hideHelpWhen={() => activeTab !== TabID.SUBSTITUTION}
>
<Overlay position='top-0 right-0'>
<BadgeHelp topic={HelpTopic.CC_OSS} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} offset={14} />
</Overlay>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col'

View File

@ -4,14 +4,11 @@ import clsx from 'clsx';
import { useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import { ReferenceType } from '@/models/language';
import { HelpTopic } from '@/models/miscellaneous';
import { IRSForm } from '@/models/rsform';
import { PARAMETER } from '@/utils/constants';
import { labelReferenceType } from '@/utils/labels';
import TabEntityReference from './TabEntityReference';
@ -71,15 +68,8 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
canSubmit={isValid}
onSubmit={handleSubmit}
className='w-[40rem] px-6 h-[32rem]'
helpTopic={HelpTopic.TERM_CONTROL}
>
<Overlay position='top-0 right-0'>
<BadgeHelp
topic={HelpTopic.TERM_CONTROL}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
offset={14}
/>
</Overlay>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col'

View File

@ -4,19 +4,16 @@ import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react';
import { IconAccept, IconMoveDown, IconMoveLeft, IconMoveRight, IconRemove } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import SelectMultiGrammeme from '@/components/select/SelectMultiGrammeme';
import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TextArea from '@/components/ui/TextArea';
import useConceptText from '@/hooks/useConceptText';
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/language';
import { parseGrammemes, wordFormEquals } from '@/models/languageAPI';
import { HelpTopic } from '@/models/miscellaneous';
import { IConstituenta, TermForm } from '@/models/rsform';
import { PARAMETER } from '@/utils/constants';
import { prompts } from '@/utils/labels';
import { IGrammemeOption, SelectorGrammemes, SelectorGrammemesList } from '@/utils/selectors';
@ -130,15 +127,8 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
submitText='Сохранить'
onSubmit={handleSubmit}
className='flex flex-col w-[40rem] px-6'
helpTopic={HelpTopic.TERM_CONTROL}
>
<Overlay position='top-[-0.2rem] left-[8rem]'>
<BadgeHelp
topic={HelpTopic.TERM_CONTROL}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
offset={3}
/>
</Overlay>
<TextArea
disabled
spellCheck

View File

@ -1,15 +1,18 @@
'use client';
import clsx from 'clsx';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { RelocateUpIcon } from '@/components/DomainIcons';
import PickMultiConstituenta from '@/components/select/PickMultiConstituenta';
import SelectLibraryItem from '@/components/select/SelectLibraryItem';
import MiniButton from '@/components/ui/MiniButton';
import Modal, { ModalProps } from '@/components/ui/Modal';
import DataLoader from '@/components/wrap/DataLoader';
import { useLibrary } from '@/context/LibraryContext';
import useRSFormDetails from '@/hooks/useRSFormDetails';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous';
import { ICstRelocateData, IOperation, IOperationSchema } from '@/models/oss';
import { getRelocateCandidates } from '@/models/ossAPI';
import { ConstituentaID } from '@/models/rsform';
@ -17,41 +20,57 @@ import { prefixes } from '@/utils/constants';
interface DlgRelocateConstituentsProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema;
target: IOperation;
initialTarget?: IOperation;
onSubmit: (data: ICstRelocateData) => void;
}
function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgRelocateConstituentsProps) {
function DlgRelocateConstituents({ oss, hideWindow, initialTarget, onSubmit }: DlgRelocateConstituentsProps) {
const library = useLibrary();
const schemas = useMemo(() => {
const node = oss.graph.at(target.id)!;
const ids: LibraryItemID[] = [
...node.inputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null),
...node.outputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null)
];
return ids.map(id => library.items.find(item => item.id === id)).filter(item => item !== undefined);
}, [oss, library.items, target.id]);
const [directionUp, setDirectionUp] = useState(true);
const [destination, setDestination] = useState<ILibraryItem | undefined>(undefined);
const [selected, setSelected] = useState<ConstituentaID[]>([]);
const [source, setSource] = useState<ILibraryItem | undefined>(
library.items.find(item => item.id === initialTarget?.result)
);
const source = useRSFormDetails({ target: String(target.result!) });
const filtered = useMemo(() => {
if (!source.schema || !destination) {
const operation = useMemo(() => oss.items.find(item => item.result === source?.id), [oss, source]);
const sourceSchemas = useMemo(() => library.items.filter(item => oss.schemas.includes(item.id)), [library, oss]);
const destinationSchemas = useMemo(() => {
if (!operation) {
return [];
}
const node = oss.graph.at(operation.id)!;
const ids: LibraryItemID[] = directionUp
? node.inputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null)
: node.outputs.map(id => oss.operationByID.get(id)!.result).filter(id => id !== null);
return ids.map(id => library.items.find(item => item.id === id)).filter(item => item !== undefined);
}, [oss, library.items, operation, directionUp]);
const sourceData = useRSFormDetails({ target: source ? String(source.id) : undefined });
const filteredConstituents = useMemo(() => {
if (!sourceData.schema || !destination || !operation) {
return [];
}
const destinationOperation = oss.items.find(item => item.result === destination.id);
return getRelocateCandidates(target.id, destinationOperation!.id, source.schema, oss);
}, [destination, target.id, source.schema, oss]);
return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss);
}, [destination, operation, sourceData.schema, oss]);
const isValid = useMemo(() => !!destination && selected.length > 0, [destination, selected]);
const toggleDirection = useCallback(() => {
setDirectionUp(prev => !prev);
setDestination(undefined);
}, []);
useLayoutEffect(() => {
const handleSelectSource = useCallback((newValue: ILibraryItem | undefined) => {
setSource(newValue);
setDestination(undefined);
setSelected([]);
}, [destination]);
}, []);
const handleSelectDestination = useCallback((newValue: ILibraryItem | undefined) => {
setDestination(newValue);
setSelected([]);
}, []);
const handleSubmit = useCallback(() => {
@ -67,24 +86,44 @@ function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgReloc
return (
<Modal
header='Перемещение конституент'
header='Перенос конституент'
submitText='Переместить'
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
className={clsx('w-[40rem] h-[33rem]', 'py-3 px-6')}
helpTopic={HelpTopic.UI_RELOCATE_CST}
>
<DataLoader id='dlg-relocate-constituents' className='cc-column' isLoading={source.loading} error={source.error}>
<div className='flex flex-col border'>
<div className='flex gap-1 items-center clr-input border-b rounded-t-md'>
<SelectLibraryItem
noBorder
className='w-1/2'
placeholder='Выберите исходную схему'
items={sourceSchemas}
value={source}
onSelectValue={handleSelectSource}
/>
<MiniButton
title='Направление перемещения'
icon={<RelocateUpIcon value={directionUp} />}
onClick={toggleDirection}
/>
<SelectLibraryItem
noBorder
className='w-1/2'
placeholder='Выберите целевую схему'
items={schemas}
items={destinationSchemas}
value={destination}
onSelectValue={handleSelectDestination}
/>
{source.schema ? (
</div>
<DataLoader id='dlg-relocate-constituents' isLoading={sourceData.loading} error={sourceData.error}>
{sourceData.schema ? (
<PickMultiConstituenta
schema={source.schema}
data={filtered}
noBorder
schema={sourceData.schema}
data={filteredConstituents}
rows={12}
prefixID={prefixes.dlg_cst_constituents_list}
selected={selected}
@ -92,6 +131,7 @@ function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgReloc
/>
) : null}
</DataLoader>
</div>
</Modal>
);
}

View File

@ -3,7 +3,6 @@
import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal, { ModalProps } from '@/components/ui/Modal';
import SelectSingle from '@/components/ui/SelectSingle';
import TextInput from '@/components/ui/TextInput';
@ -12,7 +11,6 @@ import usePartialUpdate from '@/hooks/usePartialUpdate';
import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstRenameData } from '@/models/rsform';
import { generateAlias, validateNewAlias } from '@/models/rsformAPI';
import { PARAMETER } from '@/utils/constants';
import { labelCstType } from '@/utils/labels';
import { SelectorCstType } from '@/utils/selectors';
@ -48,6 +46,7 @@ function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRen
canSubmit={validated}
onSubmit={() => onRename(cstData)}
className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex gap-3 justify-center items-center ')}
helpTopic={HelpTopic.CC_CONSTITUENTA}
>
<SelectSingle
id='dlg_cst_type'
@ -70,11 +69,6 @@ function DlgRenameCst({ hideWindow, initial, allowChangeType, onRename }: DlgRen
value={cstData.alias}
onChange={event => updateData({ alias: event.target.value })}
/>
<BadgeHelp
topic={HelpTopic.CC_CONSTITUENTA}
offset={16}
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
/>
</Modal>
);
}

View File

@ -2,7 +2,6 @@
import { useCallback, useMemo, useState } from 'react';
import BadgeHelp from '@/components/info/BadgeHelp';
import GraphUI, { GraphEdge, GraphNode } from '@/components/ui/GraphUI';
import Modal, { ModalProps } from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
@ -11,7 +10,7 @@ import { HelpTopic } from '@/models/miscellaneous';
import { SyntaxTree } from '@/models/rslang';
import { graphDarkT, graphLightT } from '@/styling/color';
import { colorBgSyntaxTree } from '@/styling/color';
import { PARAMETER, resources } from '@/utils/constants';
import { resources } from '@/utils/constants';
import { labelSyntaxTree } from '@/utils/labels';
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
@ -57,10 +56,8 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
readonly
hideWindow={hideWindow}
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
helpTopic={HelpTopic.UI_FORMULA_TREE}
>
<Overlay position='left-[0.5rem] top-[0.25rem]'>
<BadgeHelp topic={HelpTopic.UI_FORMULA_TREE} className={PARAMETER.TOOLTIP_WIDTH} />
</Overlay>
<Overlay
position='top-2 right-1/2 translate-x-1/2'
className='px-2 py-1 rounded-2xl cc-blur max-w-[60ch] text-lg text-center'

View File

@ -5,6 +5,7 @@ import { useMemo, useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions';
import Modal, { ModalProps } from '@/components/ui/Modal';
import { HelpTopic } from '@/models/miscellaneous';
import { ICstSubstitute, ICstSubstituteData } from '@/models/oss';
import { IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
@ -35,6 +36,7 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
canSubmit={canSubmit}
onSubmit={handleSubmit}
className={clsx('w-[40rem]', 'px-6 pb-3')}
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
>
<PickSubstitutions
allowSelfSubstitution

View File

@ -4,8 +4,12 @@ import { useEffect } from 'react';
import { assertIsNode } from '@/utils/utils';
function useClickedOutside({ ref, callback }: { ref: React.RefObject<HTMLElement>; callback?: () => void }) {
function useClickedOutside(enabled: boolean, ref: React.RefObject<HTMLElement>, callback?: () => void) {
useEffect(() => {
if (!enabled) {
return;
}
function handleClickOutside(event: MouseEvent) {
assertIsNode(event.target);
if (ref.current && !ref.current.contains(event.target)) {
@ -16,7 +20,7 @@ function useClickedOutside({ ref, callback }: { ref: React.RefObject<HTMLElement
return () => {
document.removeEventListener('mouseup', handleClickOutside);
};
}, [ref, callback]);
}, [ref, callback, enabled]);
}
export default useClickedOutside;

View File

@ -8,7 +8,7 @@ function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
useClickedOutside({ ref, callback: () => setIsOpen(false) });
useClickedOutside(isOpen, ref, () => setIsOpen(false));
return {
ref,

View File

@ -74,6 +74,8 @@ export enum HelpTopic {
UI_CST_STATUS = 'ui-rsform-cst-status',
UI_CST_CLASS = 'ui-rsform-cst-class',
UI_OSS_GRAPH = 'ui-oss-graph',
UI_SUBSTITUTIONS = 'ui-substitutions',
UI_RELOCATE_CST = 'ui-relocate-cst',
CONCEPTUAL = 'concept',
CC_SYSTEM = 'concept-rsform',
@ -122,6 +124,8 @@ export const topicParent = new Map<HelpTopic, HelpTopic>([
[HelpTopic.UI_CST_STATUS, HelpTopic.INTERFACE],
[HelpTopic.UI_CST_CLASS, HelpTopic.INTERFACE],
[HelpTopic.UI_OSS_GRAPH, HelpTopic.INTERFACE],
[HelpTopic.UI_SUBSTITUTIONS, HelpTopic.INTERFACE],
[HelpTopic.UI_RELOCATE_CST, HelpTopic.INTERFACE],
[HelpTopic.CONCEPTUAL, HelpTopic.CONCEPTUAL],
[HelpTopic.CC_SYSTEM, HelpTopic.CONCEPTUAL],

View File

@ -31,11 +31,13 @@ import HelpCstStatus from './items/ui/HelpCstStatus';
import HelpFormulaTree from './items/ui/HelpFormulaTree';
import HelpLibrary from './items/ui/HelpLibrary';
import HelpOssGraph from './items/ui/HelpOssGraph';
import HelpRelocateCst from './items/ui/HelpRelocateCst';
import HelpRSCard from './items/ui/HelpRSCard';
import HelpRSEditor from './items/ui/HelpRSEditor';
import HelpRSGraphTerm from './items/ui/HelpRSGraphTerm';
import HelpRSList from './items/ui/HelpRSList';
import HelpRSMenu from './items/ui/HelpRSMenu';
import HelpSubstitutions from './items/ui/HelpSubstitutions';
// PDF Viewer setup
const OFFSET_X_SMALL = 32;
@ -65,6 +67,8 @@ function TopicPage({ topic }: TopicPageProps) {
if (topic === HelpTopic.UI_CST_STATUS) return <HelpCstStatus />;
if (topic === HelpTopic.UI_CST_CLASS) return <HelpCstClass />;
if (topic === HelpTopic.UI_OSS_GRAPH) return <HelpOssGraph />;
if (topic === HelpTopic.UI_SUBSTITUTIONS) return <HelpSubstitutions />;
if (topic === HelpTopic.UI_RELOCATE_CST) return <HelpRelocateCst />;
if (topic === HelpTopic.CONCEPTUAL) return <HelpConcept />;
if (topic === HelpTopic.CC_SYSTEM) return <HelpConceptSystem />;

View File

@ -1,6 +1,7 @@
import {
IconAnimation,
IconAnimationOff,
IconChild,
IconConnect,
IconConsolidation,
IconDestroy,
@ -103,6 +104,10 @@ function HelpOssGraph() {
<li>
<IconConnect className='inline-icon' /> Выбрать КС для загрузки
</li>
<li>
<IconChild className='inline-icon icon-green' />{' '}
<LinkTopic text='Перенести конституенты' topic={HelpTopic.UI_RELOCATE_CST} />
</li>
<li>
<IconExecute className='inline-icon icon-green' /> Активировать операцию
</li>

View File

@ -0,0 +1,32 @@
import { IconMoveDown, IconMoveUp, IconPredecessor } from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous';
function HelpRelocateCst() {
return (
<div className='text-justify'>
<h1>Перенос конституент</h1>
<p>
Перенос конституент операция, при которой выбранные конституенты переносятся в другую КС в рамках одной
<LinkTopic text='операционной схемы синтеза' topic={HelpTopic.CC_OSS} />.
</p>
<li>
только для <IconPredecessor size='1rem' className='inline-icon' /> собственных конституент схемы-источника
</li>
<li>
<IconMoveUp size='1rem' className='inline-icon' />
<IconMoveDown size='1rem' className='inline-icon' /> направление переноса - вверх или вниз по дереву синтеза
</li>
<li>
при переносе вверх собственные конституенты становятся наследованными, а их копии добавляются в целевую КС
</li>
<li>
при переносе вниз собственные конституенты становятся собственными конституентами целевой КС и удаляются из
исходной КС
</li>
<li>при переносе вверх нельзя выбирать конституенты, зависящие от конституент КС, отличных от целевой</li>
</div>
);
}
export default HelpRelocateCst;

View File

@ -0,0 +1,23 @@
function HelpSubstitutions() {
return (
<div>
<h1>Таблица отождествлений</h1>
<p>Пара отождествлений, обозначает замену вхождений одной конституенты на другую.</p>
<p>
Таблица отождествлений накладывает следующие ограничения:
<li>конституента может быть удаляемой только в одном отождествлении</li>
<li>удаляемые конституенты не могут быть замещающими в отождествлениях</li>
<li>базисные множества могут замещать только другие базисные множества</li>
<li>константные множества могут замещать только другие константные множества</li>
<li>
при отождествлении конституент, отличных от базисных и константных множеств, их типизации должны совпадать с
учетом других отождествлений
</li>
<li>логические выражения могут замещать только другие логические выражения</li>
<li>при отождествлении параметризованных конституент количество и типизации операндов должно совпадать</li>
</p>
</div>
);
}
export default HelpSubstitutions;

View File

@ -77,7 +77,7 @@ function NodeContextMenu({
onHide();
}, [onHide]);
useClickedOutside({ ref, callback: handleHide });
useClickedOutside(isOpen, ref, handleHide);
useEffect(() => setIsOpen(true), []);
@ -174,7 +174,7 @@ function NodeContextMenu({
{controller.isMutable && operation.result ? (
<DropdownButton
text='Конституенты'
titleHtml='Перемещение конституент</br>между схемами'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleRelocateConstituents}

View File

@ -9,7 +9,6 @@ import {
getViewportForBounds,
Node,
NodeChange,
NodeTypes,
ReactFlow,
useEdgesState,
useNodesState,
@ -29,9 +28,8 @@ import { PARAMETER, storage } from '@/utils/constants';
import { errors } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
import InputNode from './InputNode';
import { OssNodeTypes } from './graph/OssNodeTypes';
import NodeContextMenu, { ContextMenuData } from './NodeContextMenu';
import OperationNode from './OperationNode';
import ToolbarOssGraph from './ToolbarOssGraph';
interface OssFlowProps {
@ -312,14 +310,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
}
}
const OssNodeTypes: NodeTypes = useMemo(
() => ({
synthesis: OperationNode,
input: InputNode
}),
[]
);
const graph = useMemo(
() => (
<ReactFlow
@ -328,6 +318,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange}
onNodeDoubleClick={handleNodeDoubleClick}
edgesFocusable={false}
nodesFocusable={false}
fitView
nodeTypes={OssNodeTypes}
maxZoom={2}
@ -349,7 +341,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleClickCanvas,
onEdgesChange,
handleNodeDoubleClick,
OssNodeTypes,
showGrid
]
);

View File

@ -9,7 +9,7 @@ import { OperationType } from '@/models/oss';
import { PARAMETER, prefixes } from '@/utils/constants';
import { truncateToLastWord } from '@/utils/utils';
import { useOssEdit } from '../OssEditContext';
import { useOssEdit } from '../../OssEditContext';
interface NodeCoreProps {
node: OssNodeInternal;

View File

@ -0,0 +1,9 @@
import { NodeTypes } from 'reactflow';
import InputNode from './InputNode';
import OperationNode from './OperationNode';
export const OssNodeTypes: NodeTypes = {
synthesis: OperationNode,
input: InputNode
};

View File

@ -4,6 +4,7 @@ import { urls } from '@/app/urls';
import {
IconAdmin,
IconAlert,
IconChild,
IconDestroy,
IconEdit2,
IconEditor,
@ -67,6 +68,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
router.push(urls.login);
}
function handleRelocate() {
editMenu.hide();
controller.promptRelocateConstituents(undefined, []);
}
return (
<div className='flex'>
<div ref={schemaMenu.ref}>
@ -128,9 +134,11 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
/>
<Dropdown isOpen={editMenu.isOpen}>
<DropdownButton
text='см. Граф синтеза'
titleHtml='Редактирование доступно <br/>через Граф синтеза'
disabled
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={handleRelocate}
/>
</Dropdown>
</div>

View File

@ -76,7 +76,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: OperationID, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
}
const OssEditContext = createContext<IOssEditContext | null>(null);
@ -360,7 +360,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
[model]
);
const promptRelocateConstituents = useCallback((target: OperationID, positions: IOperationPosition[]) => {
const promptRelocateConstituents = useCallback((target: OperationID | undefined, positions: IOperationPosition[]) => {
setPositions(positions);
setTargetOperationID(target);
setShowRelocateConstituents(true);
@ -368,9 +368,18 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
const handleRelocateConstituents = useCallback(
(data: ICstRelocateData) => {
if (
positions.every(item => {
const operation = model.schema!.operationByID.get(item.id)!;
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
model.relocateConstituents(data, () => toast.success(information.changesSaved));
} else {
model.savePositions({ positions: positions }, () =>
model.relocateConstituents(data, () => toast.success(information.changesSaved))
);
}
},
[model, positions]
);
@ -458,7 +467,7 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit
{showRelocateConstituents ? (
<DlgRelocateConstituents
hideWindow={() => setShowRelocateConstituents(false)}
target={targetOperation!}
initialTarget={targetOperation}
oss={model.schema}
onSubmit={handleRelocateConstituents}
/>

View File

@ -124,7 +124,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
disabled={isModified || controller.isProcessing || accessLevel < UserLevel.OWNER}
/>
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} header='Редакторы' />
</Tooltip>
<ValueIcon

View File

@ -43,6 +43,7 @@
.react-flow__handle {
cursor: default !important;
border-radius: 9999px;
border-color: var(--cl-bg-40);
background-color: var(--cl-bg-120);
@ -66,6 +67,9 @@
}
.react-flow__attribution {
font-size: var(--font-size-sm);
font-family: var(--font-ui);
background-color: transparent;
color: var(--cl-fg-60);
.dark & {

View File

@ -377,6 +377,8 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.UI_CST_STATUS: return 'Статус конституенты';
case HelpTopic.UI_CST_CLASS: return 'Класс конституенты';
case HelpTopic.UI_OSS_GRAPH: return 'Граф синтеза';
case HelpTopic.UI_SUBSTITUTIONS:return 'Отождествления';
case HelpTopic.UI_RELOCATE_CST: return 'Перенос конституент';
case HelpTopic.CONCEPTUAL: return '♨️ Концептуализация';
case HelpTopic.CC_SYSTEM: return 'Система определений';
@ -428,6 +430,8 @@ export function describeHelpTopic(topic: HelpTopic): string {
case HelpTopic.UI_CST_STATUS: return 'нотация статуса конституенты';
case HelpTopic.UI_CST_CLASS: return 'нотация класса конституенты';
case HelpTopic.UI_OSS_GRAPH: return 'графическая форма <br/>операционной схемы синтеза';
case HelpTopic.UI_SUBSTITUTIONS:return 'таблица отождествлений конституент';
case HelpTopic.UI_RELOCATE_CST: return 'перенос конституент<br/>в рамках ОСС';
case HelpTopic.CONCEPTUAL: return 'основы концептуализации';
case HelpTopic.CC_SYSTEM: return 'концептуальная схема <br/>как система понятий';
@ -435,7 +439,7 @@ export function describeHelpTopic(topic: HelpTopic): string {
case HelpTopic.CC_RELATIONS: return 'отношения между конституентами';
case HelpTopic.CC_SYNTHESIS: return 'операция синтеза концептуальных схем';
case HelpTopic.CC_OSS: return 'операционная схема синтеза';
case HelpTopic.CC_PROPAGATION: return 'протаскивание изменений в ОСС';
case HelpTopic.CC_PROPAGATION: return 'сквозные изменения в ОСС';
case HelpTopic.RSLANG: return 'экспликация и язык родов структур';
case HelpTopic.RSL_TYPES: return 'система типов в <br/>родоструктурной экспликации';