mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactor UI elements
This commit is contained in:
parent
286bb4f29d
commit
da05bd6a12
|
@ -1,23 +1,29 @@
|
||||||
import { MagnifyingGlassIcon } from '../Icons';
|
import { MagnifyingGlassIcon } from '../Icons';
|
||||||
|
import Overlay from './Overlay';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|
||||||
interface ConceptSearchProps {
|
interface ConceptSearchProps {
|
||||||
value: string
|
value: string
|
||||||
onChange?: (newValue: string) => void
|
onChange?: (newValue: string) => void
|
||||||
dense?: boolean
|
dense?: boolean
|
||||||
|
noBorder?: boolean
|
||||||
|
dimensions?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConceptSearch({ value, onChange, dense }: ConceptSearchProps) {
|
function ConceptSearch({ value, onChange, noBorder, dimensions, dense }: ConceptSearchProps) {
|
||||||
const borderClass = dense ? 'border-t border-x': '';
|
const borderClass = dense && !noBorder ? 'border-t border-x': '';
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className={dimensions}>
|
||||||
<div className='absolute inset-y-0 flex items-center pl-3 pointer-events-none text-controls'>
|
<Overlay
|
||||||
<MagnifyingGlassIcon />
|
position='top-0 left-3 translate-y-1/2'
|
||||||
</div>
|
className='flex items-center pointer-events-none text-controls'
|
||||||
|
>
|
||||||
|
<MagnifyingGlassIcon size={5} />
|
||||||
|
</Overlay>
|
||||||
<TextInput noOutline
|
<TextInput noOutline
|
||||||
placeholder='Поиск'
|
placeholder='Поиск'
|
||||||
dimensions={`w-full pl-10 ${borderClass}`}
|
dimensions={`w-full pl-10 hover:text-clip outline-none ${borderClass}`}
|
||||||
noBorder={dense}
|
noBorder={dense || noBorder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
|
||||||
import { useConceptTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
import { selectDarkT, selectLightT } from '../../utils/color';
|
import { selectDarkT, selectLightT } from '../../utils/color';
|
||||||
|
|
||||||
interface SelectMultiProps<
|
export interface SelectMultiProps<
|
||||||
Option,
|
Option,
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
>
|
>
|
||||||
|
|
|
@ -19,7 +19,7 @@ function TextInput({
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
...restProps
|
...restProps
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
const borderClass = noBorder ? '' : 'border';
|
const borderClass = noBorder ? '' : 'border px-3';
|
||||||
const outlineClass = noOutline ? '' : 'clr-outline';
|
const outlineClass = noOutline ? '' : 'clr-outline';
|
||||||
return (
|
return (
|
||||||
<div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}>
|
<div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}>
|
||||||
|
@ -31,7 +31,7 @@ function TextInput({
|
||||||
<input id={id}
|
<input id={id}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
||||||
className={`px-3 py-2 leading-tight truncate hover:text-clip ${colors} ${outlineClass} ${borderClass} ${dense ? 'w-full' : dimensions}`}
|
className={`py-2 leading-tight truncate hover:text-clip ${colors} ${outlineClass} ${borderClass} ${dense ? 'w-full' : dimensions}`}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
@ -4,16 +4,19 @@ interface TextURLProps {
|
||||||
text: string
|
text: string
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
href?: string
|
href?: string
|
||||||
|
color?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextURL({ text, href, tooltip, onClick }: TextURLProps) {
|
function TextURL({ text, href, tooltip, color='text-url', onClick }: TextURLProps) {
|
||||||
|
const design = `cursor-pointer hover:underline ${color}`;
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className='cursor-pointer hover:underline text-url'
|
className={design}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
to={href}
|
to={href}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -21,8 +24,9 @@ function TextURL({ text, href, tooltip, onClick }: TextURLProps) {
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className='cursor-pointer hover:underline text-url'
|
className={design}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</span>);
|
</span>);
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { urls } from '../utils/constants';
|
import { urls } from '../utils/constants';
|
||||||
|
import TextURL from './Common/TextURL';
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className='px-4 py-2 text-sm select-none z-navigation whitespace-nowrap clr-footer'>
|
<footer tabIndex={-1} className='flex flex-col items-center w-full gap-1 px-4 py-2 text-sm select-none z-navigation whitespace-nowrap'>
|
||||||
<div className='justify-center w-full mx-auto'>
|
<div className='flex gap-3 text-center'>
|
||||||
<div className='mb-2 text-center'>
|
<TextURL text='Библиотека' href='/library' color='clr-footer'/>
|
||||||
<Link className='mx-2 hover:underline' to='/library' tabIndex={-1}>Библиотека</Link>
|
<TextURL text='Справка' href='/manuals' color='clr-footer'/>
|
||||||
<Link className='mx-2 hover:underline' to='/manuals' tabIndex={-1}>Справка</Link>
|
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer'/>
|
||||||
<Link className='mx-2 hover:underline' to={urls.concept} tabIndex={-1}>Центр Концепт</Link>
|
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer'/>
|
||||||
<Link className='mx-2 hover:underline' to='/manuals?topic=exteor' tabIndex={-1}>Экстеор</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className='mt-0.5 text-center'>© 2023 ЦИВТ КОНЦЕПТ</p>
|
<p className='mt-0.5 text-center clr-footer'>© 2023 ЦИВТ КОНЦЕПТ</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</footer>);
|
</footer>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,9 @@ function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) {
|
||||||
</svg>);
|
</svg>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MagnifyingGlassIcon({ size, ...props }: IconProps) {
|
export function MagnifyingGlassIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<IconSVG viewbox='0 0 20 20' size={size ?? 5} {...props} >
|
<IconSVG viewbox='0 0 20 20' {...props} >
|
||||||
<path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/>
|
<path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/>
|
||||||
</IconSVG>
|
</IconSVG>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import UserMenu from './UserMenu';
|
||||||
|
|
||||||
function Navigation () {
|
function Navigation () {
|
||||||
const { navigateTo } = useConceptNavigation();
|
const { navigateTo } = useConceptNavigation();
|
||||||
const { noNavigation, toggleNoNavigation } = useConceptTheme();
|
const { noNavigation } = useConceptTheme();
|
||||||
|
|
||||||
const navigateLibrary = () => navigateTo('/library');
|
const navigateLibrary = () => navigateTo('/library');
|
||||||
const navigateHelp = () => navigateTo('/manuals');
|
const navigateHelp = () => navigateTo('/manuals');
|
||||||
|
@ -16,7 +16,7 @@ function Navigation () {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='sticky top-0 left-0 right-0 select-none clr-app z-navigation h-fit'>
|
<nav className='sticky top-0 left-0 right-0 select-none clr-app z-navigation h-fit'>
|
||||||
<ToggleNavigationButton noNavigation={noNavigation} toggleNoNavigation={toggleNoNavigation} />
|
<ToggleNavigationButton />
|
||||||
{!noNavigation ?
|
{!noNavigation ?
|
||||||
<div className='flex items-stretch justify-between pl-2 pr-[0.8rem] border-b-2 rounded-none h-[3rem]'>
|
<div className='flex items-stretch justify-between pl-2 pr-[0.8rem] border-b-2 rounded-none h-[3rem]'>
|
||||||
<div className='flex items-center justify-start'>
|
<div className='flex items-center justify-start'>
|
||||||
|
|
|
@ -1,28 +1,23 @@
|
||||||
interface ToggleNavigationButtonProps {
|
import { useMemo } from 'react';
|
||||||
noNavigation?: boolean
|
|
||||||
toggleNoNavigation: () => void
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
}
|
|
||||||
|
function ToggleNavigationButton() {
|
||||||
|
const { noNavigation, toggleNoNavigation } = useConceptTheme();
|
||||||
|
const dimensions = useMemo(() => (noNavigation ? 'px-1 h-[1.6rem]' : 'w-[1.2rem] h-[3rem]'), [noNavigation]);
|
||||||
|
const text = useMemo(() => (
|
||||||
|
noNavigation ? '∨∨∨' : <><p>{'>'}</p><p>{'>'}</p></>), [noNavigation]
|
||||||
|
);
|
||||||
|
const tooltip = useMemo(() => (noNavigation ? 'Показать навигацию' : 'Скрыть навигацию'), [noNavigation]);
|
||||||
|
|
||||||
function ToggleNavigationButton({ noNavigation, toggleNoNavigation }: ToggleNavigationButtonProps) {
|
|
||||||
if (noNavigation) {
|
|
||||||
return (
|
return (
|
||||||
<button type='button' tabIndex={-1}
|
<button type='button' tabIndex={-1}
|
||||||
title='Показать навигацию'
|
title={tooltip}
|
||||||
className='absolute top-0 right-0 z-navigation px-1 h-[1.6rem] border-b-2 border-l-2 clr-btn-nav rounded-none'
|
className={`absolute top-0 right-0 border-b-2 border-l-2 rounded-none z-navigation clr-btn-nav ${dimensions}`}
|
||||||
onClick={toggleNoNavigation}
|
onClick={toggleNoNavigation}
|
||||||
>
|
>
|
||||||
{'∨∨∨'}
|
{text}
|
||||||
</button>);
|
</button>);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<button type='button' tabIndex={-1}
|
|
||||||
title='Скрыть навигацию'
|
|
||||||
className='absolute top-0 right-0 z-navigation w-[1.2rem] h-[3rem] border-b-2 border-l-2 clr-btn-nav rounded-none'
|
|
||||||
onClick={toggleNoNavigation}
|
|
||||||
>
|
|
||||||
<p>{'>'}</p><p>{'>'}</p>
|
|
||||||
</button>);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ToggleNavigationButton;
|
export default ToggleNavigationButton;
|
|
@ -6,7 +6,6 @@ import { TokenID } from '../../models/rslang';
|
||||||
import { CodeMirrorWrapper } from '../../utils/codemirror';
|
import { CodeMirrorWrapper } from '../../utils/codemirror';
|
||||||
|
|
||||||
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
||||||
console.log(keyCode);
|
|
||||||
if (shiftPressed) {
|
if (shiftPressed) {
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case 'Backquote': return '∃';
|
case 'Backquote': return '∃';
|
||||||
|
|
|
@ -81,7 +81,7 @@ function ConstituentaPicker({
|
||||||
}], [value, colors]);
|
}], [value, colors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<ConceptSearch dense
|
<ConceptSearch dense
|
||||||
value={filterText}
|
value={filterText}
|
||||||
onChange={newValue => setFilterText(newValue)}
|
onChange={newValue => setFilterText(newValue)}
|
||||||
|
@ -103,7 +103,7 @@ function ConstituentaPicker({
|
||||||
onRowClicked={onSelectValue}
|
onRowClicked={onSelectValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ConstituentaPicker;
|
export default ConstituentaPicker;
|
||||||
|
|
29
rsconcept/frontend/src/components/Shared/GrammemeBadge.tsx
Normal file
29
rsconcept/frontend/src/components/Shared/GrammemeBadge.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
import { GramData } from '../../models/language';
|
||||||
|
import { colorfgGrammeme } from '../../utils/color';
|
||||||
|
import { labelGrammeme } from '../../utils/labels';
|
||||||
|
|
||||||
|
interface GrammemeBadgeProps {
|
||||||
|
key?: string
|
||||||
|
grammeme: GramData
|
||||||
|
}
|
||||||
|
|
||||||
|
function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
|
||||||
|
const { colors } = useConceptTheme();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
|
||||||
|
style={{
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderColor: colorfgGrammeme(grammeme, colors),
|
||||||
|
color: colorfgGrammeme(grammeme, colors),
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: colors.bgInput
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelGrammeme(grammeme)}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GrammemeBadge;
|
43
rsconcept/frontend/src/components/Shared/SelectGrammeme.tsx
Normal file
43
rsconcept/frontend/src/components/Shared/SelectGrammeme.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Grammeme } from '../../models/language';
|
||||||
|
import { getCompatibleGrams } from '../../models/languageAPI';
|
||||||
|
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammems } from '../../utils/selectors';
|
||||||
|
import SelectMulti, { SelectMultiProps } from '../Common/SelectMulti';
|
||||||
|
|
||||||
|
interface SelectGrammemeProps extends
|
||||||
|
Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
|
||||||
|
value: IGrammemeOption[]
|
||||||
|
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
|
||||||
|
dimensions?: string
|
||||||
|
className?: string
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGrammeme({
|
||||||
|
value, setValue,
|
||||||
|
dimensions, className, placeholder
|
||||||
|
}: SelectGrammemeProps) {
|
||||||
|
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
const compatible = getCompatibleGrams(
|
||||||
|
value
|
||||||
|
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
|
||||||
|
.map(data => data.value as Grammeme)
|
||||||
|
);
|
||||||
|
setOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectMulti
|
||||||
|
className={`${dimensions} ${className}`}
|
||||||
|
options={options}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectGrammeme;
|
22
rsconcept/frontend/src/components/Shared/WordFormBadge.tsx
Normal file
22
rsconcept/frontend/src/components/Shared/WordFormBadge.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { IWordForm } from '../../models/language';
|
||||||
|
import GrammemeBadge from './GrammemeBadge';
|
||||||
|
|
||||||
|
interface WordFormBadgeProps {
|
||||||
|
keyPrefix?: string
|
||||||
|
form: IWordForm
|
||||||
|
}
|
||||||
|
|
||||||
|
function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-wrap justify-start gap-1 select-none'>
|
||||||
|
{form.grams.map(
|
||||||
|
(gram) =>
|
||||||
|
<GrammemeBadge
|
||||||
|
key={`${keyPrefix}-${gram}`}
|
||||||
|
grammeme={gram}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WordFormBadge;
|
|
@ -1,18 +1,18 @@
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import Label from '../../components/Common/Label';
|
import Label from '../../components/Common/Label';
|
||||||
import SelectMulti from '../../components/Common/SelectMulti';
|
|
||||||
import TextInput from '../../components/Common/TextInput';
|
import TextInput from '../../components/Common/TextInput';
|
||||||
import ConstituentaPicker from '../../components/Shared/ConstituentaPicker';
|
import ConstituentaPicker from '../../components/Shared/ConstituentaPicker';
|
||||||
import { Grammeme, ReferenceType } from '../../models/language';
|
import SelectGrammeme from '../../components/Shared/SelectGrammeme';
|
||||||
import { getCompatibleGrams, parseEntityReference, parseGrammemes } from '../../models/languageAPI';
|
import { ReferenceType } from '../../models/language';
|
||||||
|
import { parseEntityReference, parseGrammemes } from '../../models/languageAPI';
|
||||||
import { CstMatchMode } from '../../models/miscelanious';
|
import { CstMatchMode } from '../../models/miscelanious';
|
||||||
import { IConstituenta } from '../../models/rsform';
|
import { IConstituenta } from '../../models/rsform';
|
||||||
import { matchConstituenta } from '../../models/rsformAPI';
|
import { matchConstituenta } from '../../models/rsformAPI';
|
||||||
import { prefixes } from '../../utils/constants';
|
import { prefixes } from '../../utils/constants';
|
||||||
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammems } from '../../utils/selectors';
|
import { IGrammemeOption, SelectorGrammems } from '../../utils/selectors';
|
||||||
import { IReferenceInputState } from './DlgEditReference';
|
import { IReferenceInputState } from './DlgEditReference';
|
||||||
import SelectTermform from './SelectTermform';
|
import SelectWordForm from './SelectWordForm';
|
||||||
|
|
||||||
interface EntityTabProps {
|
interface EntityTabProps {
|
||||||
initial: IReferenceInputState
|
initial: IReferenceInputState
|
||||||
|
@ -25,9 +25,7 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
|
||||||
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
|
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
const [alias, setAlias] = useState('');
|
const [alias, setAlias] = useState('');
|
||||||
const [term, setTerm] = useState('');
|
const [term, setTerm] = useState('');
|
||||||
|
|
||||||
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
|
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
|
||||||
const [gramOptions, setGramOptions] = useState<IGrammemeOption[]>([]);
|
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
useLayoutEffect(
|
useLayoutEffect(
|
||||||
|
@ -47,17 +45,6 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
|
||||||
setReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
setReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
||||||
}, [alias, selectedGrams, setIsValid, setReference]);
|
}, [alias, selectedGrams, setIsValid, setReference]);
|
||||||
|
|
||||||
// Filter grammemes when input changes
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const compatible = getCompatibleGrams(
|
|
||||||
selectedGrams
|
|
||||||
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
|
|
||||||
.map(data => data.value as Grammeme)
|
|
||||||
);
|
|
||||||
setGramOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
|
|
||||||
}, [selectedGrams]);
|
|
||||||
|
|
||||||
// Update term when alias changes
|
// Update term when alias changes
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
|
@ -91,30 +78,28 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
|
||||||
value={alias}
|
value={alias}
|
||||||
onChange={event => setAlias(event.target.value)}
|
onChange={event => setAlias(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className='flex items-center w-full flex-start'>
|
|
||||||
<Label text='Термин' />
|
|
||||||
<TextInput disabled dense noBorder
|
<TextInput disabled dense noBorder
|
||||||
|
label='Термин'
|
||||||
value={term}
|
value={term}
|
||||||
tooltip={term}
|
tooltip={term}
|
||||||
dimensions='w-full text-sm'
|
dimensions='w-full text-sm'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectTermform
|
<SelectWordForm
|
||||||
selected={selectedGrams}
|
selected={selectedGrams}
|
||||||
setSelected={setSelectedGrams}
|
setSelected={setSelectedGrams}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex items-center gap-4 flex-start'>
|
<div className='flex items-center gap-4 flex-start'>
|
||||||
<Label text='Отсылаемая словоформа'/>
|
<Label text='Отсылаемая словоформа'/>
|
||||||
<SelectMulti
|
<SelectGrammeme
|
||||||
placeholder='Выберите граммемы'
|
placeholder='Выберите граммемы'
|
||||||
className='flex-grow h-full'
|
dimensions='h-full '
|
||||||
|
className='flex-grow'
|
||||||
menuPlacement='top'
|
menuPlacement='top'
|
||||||
options={gramOptions}
|
|
||||||
value={selectedGrams}
|
value={selectedGrams}
|
||||||
onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))}
|
setValue={setSelectedGrams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
@ -5,12 +5,12 @@ import { prefixes } from '../../utils/constants';
|
||||||
import { IGrammemeOption, PremadeWordForms, SelectorGrammems } from '../../utils/selectors';
|
import { IGrammemeOption, PremadeWordForms, SelectorGrammems } from '../../utils/selectors';
|
||||||
import WordformButton from './WordformButton';
|
import WordformButton from './WordformButton';
|
||||||
|
|
||||||
interface SelectTermformProps {
|
interface SelectWordFormProps {
|
||||||
selected: IGrammemeOption[]
|
selected: IGrammemeOption[]
|
||||||
setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
|
setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectTermform({ selected, setSelected }: SelectTermformProps) {
|
function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(grams: Grammeme[]) => {
|
(grams: Grammeme[]) => {
|
||||||
setSelected(SelectorGrammems.filter(({value}) => grams.includes(value as Grammeme)));
|
setSelected(SelectorGrammems.filter(({value}) => grams.includes(value as Grammeme)));
|
||||||
|
@ -42,4 +42,4 @@ function SelectTermform({ selected, setSelected }: SelectTermformProps) {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectTermform;
|
export default SelectWordForm;
|
|
@ -1,316 +0,0 @@
|
||||||
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import Label from '../components/Common/Label';
|
|
||||||
import MiniButton from '../components/Common/MiniButton';
|
|
||||||
import Modal from '../components/Common/Modal';
|
|
||||||
import Overlay from '../components/Common/Overlay';
|
|
||||||
import SelectMulti from '../components/Common/SelectMulti';
|
|
||||||
import TextArea from '../components/Common/TextArea';
|
|
||||||
import DataTable, { createColumnHelper } from '../components/DataTable';
|
|
||||||
import HelpButton from '../components/Help/HelpButton';
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossIcon } from '../components/Icons';
|
|
||||||
import { useConceptTheme } from '../context/ThemeContext';
|
|
||||||
import useConceptText from '../hooks/useConceptText';
|
|
||||||
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '../models/language';
|
|
||||||
import { getCompatibleGrams, parseGrammemes,wordFormEquals } from '../models/languageAPI';
|
|
||||||
import { HelpTopic } from '../models/miscelanious';
|
|
||||||
import { IConstituenta, TermForm } from '../models/rsform';
|
|
||||||
import { colorfgGrammeme } from '../utils/color';
|
|
||||||
import { labelGrammeme } from '../utils/labels';
|
|
||||||
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../utils/selectors';
|
|
||||||
|
|
||||||
interface DlgEditWordFormsProps {
|
|
||||||
hideWindow: () => void
|
|
||||||
target: IConstituenta
|
|
||||||
onSave: (data: TermForm[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IWordForm>();
|
|
||||||
|
|
||||||
function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps) {
|
|
||||||
const textProcessor = useConceptText();
|
|
||||||
const { colors } = useConceptTheme();
|
|
||||||
const [term, setTerm] = useState('');
|
|
||||||
|
|
||||||
const [inputText, setInputText] = useState('');
|
|
||||||
const [inputGrams, setInputGrams] = useState<IGrammemeOption[]>([]);
|
|
||||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
|
||||||
|
|
||||||
const [forms, setForms] = useState<IWordForm[]>([]);
|
|
||||||
|
|
||||||
function getData(): TermForm[] {
|
|
||||||
const result: TermForm[] = [];
|
|
||||||
forms.forEach(
|
|
||||||
({text, grams}) => result.push({
|
|
||||||
text: text,
|
|
||||||
tags: grams.join(',')
|
|
||||||
}));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
useLayoutEffect(
|
|
||||||
() => {
|
|
||||||
const initForms: IWordForm[] = [];
|
|
||||||
target.term_forms.forEach(
|
|
||||||
term => initForms.push({
|
|
||||||
text: term.text,
|
|
||||||
grams: parseGrammemes(term.tags),
|
|
||||||
}));
|
|
||||||
setForms(initForms);
|
|
||||||
setTerm(target.term_resolved);
|
|
||||||
setInputText(target.term_resolved);
|
|
||||||
setInputGrams([]);
|
|
||||||
}, [target]);
|
|
||||||
|
|
||||||
// Filter grammemes when input changes
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const compatible = getCompatibleGrams(
|
|
||||||
inputGrams
|
|
||||||
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
|
|
||||||
.map(data => data.value as Grammeme)
|
|
||||||
);
|
|
||||||
setOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
|
|
||||||
}, [inputGrams]);
|
|
||||||
|
|
||||||
const handleSubmit = () => onSave(getData());
|
|
||||||
|
|
||||||
function handleAddForm() {
|
|
||||||
const newForm: IWordForm = {
|
|
||||||
text: inputText,
|
|
||||||
grams: inputGrams.map(item => item.value)
|
|
||||||
};
|
|
||||||
setForms(forms => [
|
|
||||||
newForm,
|
|
||||||
...forms.filter(value => !wordFormEquals(value, newForm))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteRow(row: number) {
|
|
||||||
setForms(
|
|
||||||
(prev) => {
|
|
||||||
const newForms: IWordForm[] = [];
|
|
||||||
prev.forEach(
|
|
||||||
(form, index) => {
|
|
||||||
if (index !== row) {
|
|
||||||
newForms.push(form);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newForms;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRowClicked(form: IWordForm) {
|
|
||||||
setInputText(form.text);
|
|
||||||
setInputGrams(SelectorGrammems.filter(gram => form.grams.find(test => test === gram.value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResetAll() {
|
|
||||||
setForms([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInflect() {
|
|
||||||
const data: IWordFormPlain = {
|
|
||||||
text: term,
|
|
||||||
grams: inputGrams.map(gram => gram.value).join(',')
|
|
||||||
}
|
|
||||||
textProcessor.inflect(data, response => setInputText(response.result));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleParse() {
|
|
||||||
const data: ITextRequest = {
|
|
||||||
text: inputText
|
|
||||||
}
|
|
||||||
textProcessor.parse(data, response => {
|
|
||||||
const grams = parseGrammemes(response.result);
|
|
||||||
setInputGrams(SelectorGrammems.filter(gram => grams.find(test => test === gram.value)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGenerateLexeme() {
|
|
||||||
if (forms.length > 0) {
|
|
||||||
if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const data: ITextRequest = {
|
|
||||||
text: inputText
|
|
||||||
}
|
|
||||||
textProcessor.generateLexeme(data, response => {
|
|
||||||
const lexeme: IWordForm[] = [];
|
|
||||||
response.items.forEach(
|
|
||||||
form => {
|
|
||||||
const newForm: IWordForm = {
|
|
||||||
text: form.text,
|
|
||||||
grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme))
|
|
||||||
}
|
|
||||||
if (newForm.grams.length === 2 && !lexeme.some(test => wordFormEquals(test, newForm))) {
|
|
||||||
lexeme.push(newForm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setForms(lexeme);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor('text', {
|
|
||||||
id: 'text',
|
|
||||||
header: 'Текст',
|
|
||||||
size: 350,
|
|
||||||
minSize: 350,
|
|
||||||
maxSize: 350,
|
|
||||||
cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('grams', {
|
|
||||||
id: 'grams',
|
|
||||||
header: 'Граммемы',
|
|
||||||
size: 250,
|
|
||||||
minSize: 250,
|
|
||||||
maxSize: 250,
|
|
||||||
cell: props =>
|
|
||||||
<div className='flex flex-wrap justify-start gap-1 select-none'>
|
|
||||||
{props.getValue().map(
|
|
||||||
(gram) =>
|
|
||||||
<div
|
|
||||||
key={`${props.cell.id}-${gram}`}
|
|
||||||
className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
|
|
||||||
style={{
|
|
||||||
borderWidth: '1px',
|
|
||||||
borderColor: colorfgGrammeme(gram, colors),
|
|
||||||
color: colorfgGrammeme(gram, colors),
|
|
||||||
fontWeight: 600,
|
|
||||||
backgroundColor: colors.bgInput
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{labelGrammeme(gram)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}),
|
|
||||||
columnHelper.display({
|
|
||||||
id: 'actions',
|
|
||||||
size: 50,
|
|
||||||
minSize: 50,
|
|
||||||
maxSize: 50,
|
|
||||||
cell: props =>
|
|
||||||
<div>
|
|
||||||
<MiniButton noHover
|
|
||||||
tooltip='Удалить словоформу'
|
|
||||||
icon={<CrossIcon size={4} color='text-warning'/>}
|
|
||||||
onClick={() => handleDeleteRow(props.row.index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
], [colors]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal canSubmit
|
|
||||||
title='Редактирование словоформ'
|
|
||||||
hideWindow={hideWindow}
|
|
||||||
submitText='Сохранить'
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className='min-w-[40rem] max-w-[40rem] px-6'
|
|
||||||
>
|
|
||||||
<Overlay position='top-[-0.2rem] left-[7.5rem]'>
|
|
||||||
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
<TextArea disabled spellCheck
|
|
||||||
label='Начальная форма'
|
|
||||||
placeholder='Термин в начальной форме'
|
|
||||||
rows={1}
|
|
||||||
value={term}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='mt-3 mb-2'>
|
|
||||||
<Label text='Параметры словоформы' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-start justify-between w-full'>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<TextArea
|
|
||||||
placeholder='Введите текст'
|
|
||||||
dimensions='min-w-[18rem] w-full min-h-[5rem]'
|
|
||||||
rows={2}
|
|
||||||
value={inputText}
|
|
||||||
onChange={event => setInputText(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div className='max-w-min'>
|
|
||||||
<MiniButton
|
|
||||||
tooltip='Генерировать словоформу'
|
|
||||||
icon={<ArrowLeftIcon size={5} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'} />}
|
|
||||||
disabled={textProcessor.loading || inputGrams.length == 0}
|
|
||||||
onClick={handleInflect}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
tooltip='Определить граммемы'
|
|
||||||
icon={<ArrowRightIcon
|
|
||||||
size={5}
|
|
||||||
color={!inputText ? 'text-disabled' : 'text-primary'}
|
|
||||||
/>}
|
|
||||||
disabled={textProcessor.loading || !inputText}
|
|
||||||
onClick={handleParse}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SelectMulti
|
|
||||||
className='min-w-[20rem] max-w-[20rem] h-full flex-grow'
|
|
||||||
options={options}
|
|
||||||
placeholder='Выберите граммемы'
|
|
||||||
value={inputGrams}
|
|
||||||
onChange={newValue => setInputGrams([...newValue].sort(compareGrammemeOptions))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-between mt-2 mb-1 flex-start'>
|
|
||||||
<div className='flex items-center justify-start'>
|
|
||||||
<MiniButton
|
|
||||||
tooltip='Внести словоформу'
|
|
||||||
icon={<CheckIcon
|
|
||||||
size={5}
|
|
||||||
color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}
|
|
||||||
/>}
|
|
||||||
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
|
|
||||||
onClick={handleAddForm}
|
|
||||||
/>
|
|
||||||
<MiniButton
|
|
||||||
tooltip='Генерировать стандартные словоформы'
|
|
||||||
icon={<ChevronDoubleDownIcon
|
|
||||||
size={5}
|
|
||||||
color={!inputText ? 'text-disabled' : 'text-primary'}
|
|
||||||
/>}
|
|
||||||
disabled={textProcessor.loading || !inputText}
|
|
||||||
onClick={handleGenerateLexeme}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='w-full text-sm font-semibold text-center'>
|
|
||||||
Заданные вручную словоформы [{forms.length}]
|
|
||||||
</div>
|
|
||||||
<MiniButton
|
|
||||||
tooltip='Сбросить все словоформы'
|
|
||||||
icon={<CrossIcon size={5} color={forms.length === 0 ? 'text-disabled' : 'text-warning'} />}
|
|
||||||
disabled={textProcessor.loading || forms.length === 0}
|
|
||||||
onClick={handleResetAll}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='border overflow-y-auto max-h-[17.4rem] min-h-[17.4rem] mb-2'>
|
|
||||||
<DataTable dense noFooter
|
|
||||||
data={forms}
|
|
||||||
columns={columns}
|
|
||||||
headPosition='0'
|
|
||||||
noDataComponent={
|
|
||||||
<span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'>
|
|
||||||
<p>Список пуст</p>
|
|
||||||
<p>Добавьте словоформу</p>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
onRowClicked={handleRowClicked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Modal>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DlgEditWordForms;
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Label from '../../components/Common/Label';
|
||||||
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
|
import Modal from '../../components/Common/Modal';
|
||||||
|
import Overlay from '../../components/Common/Overlay';
|
||||||
|
import TextArea from '../../components/Common/TextArea';
|
||||||
|
import HelpButton from '../../components/Help/HelpButton';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon } from '../../components/Icons';
|
||||||
|
import SelectGrammeme from '../../components/Shared/SelectGrammeme';
|
||||||
|
import useConceptText from '../../hooks/useConceptText';
|
||||||
|
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '../../models/language';
|
||||||
|
import { parseGrammemes, wordFormEquals } from '../../models/languageAPI';
|
||||||
|
import { HelpTopic } from '../../models/miscelanious';
|
||||||
|
import { IConstituenta, TermForm } from '../../models/rsform';
|
||||||
|
import { IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../../utils/selectors';
|
||||||
|
import WordFormsTable from './WordFormsTable';
|
||||||
|
|
||||||
|
interface DlgEditWordFormsProps {
|
||||||
|
hideWindow: () => void
|
||||||
|
target: IConstituenta
|
||||||
|
onSave: (data: TermForm[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps) {
|
||||||
|
const textProcessor = useConceptText();
|
||||||
|
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
const [inputText, setInputText] = useState('');
|
||||||
|
const [inputGrams, setInputGrams] = useState<IGrammemeOption[]>([]);
|
||||||
|
const [forms, setForms] = useState<IWordForm[]>([]);
|
||||||
|
|
||||||
|
useLayoutEffect(
|
||||||
|
() => {
|
||||||
|
const initForms: IWordForm[] = [];
|
||||||
|
target.term_forms.forEach(
|
||||||
|
term => initForms.push({
|
||||||
|
text: term.text,
|
||||||
|
grams: parseGrammemes(term.tags),
|
||||||
|
}));
|
||||||
|
setForms(initForms);
|
||||||
|
setTerm(target.term_resolved);
|
||||||
|
setInputText(target.term_resolved);
|
||||||
|
setInputGrams([]);
|
||||||
|
}, [target]);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const result: TermForm[] = [];
|
||||||
|
forms.forEach(
|
||||||
|
({text, grams}) => result.push({
|
||||||
|
text: text,
|
||||||
|
tags: grams.join(',')
|
||||||
|
}));
|
||||||
|
onSave(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddForm() {
|
||||||
|
const newForm: IWordForm = {
|
||||||
|
text: inputText,
|
||||||
|
grams: inputGrams.map(item => item.value)
|
||||||
|
};
|
||||||
|
setForms(forms => [
|
||||||
|
newForm,
|
||||||
|
...forms.filter(value => !wordFormEquals(value, newForm))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectForm(form: IWordForm) {
|
||||||
|
setInputText(form.text);
|
||||||
|
setInputGrams(SelectorGrammems.filter(gram => form.grams.find(test => test === gram.value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInflect() {
|
||||||
|
const data: IWordFormPlain = {
|
||||||
|
text: term,
|
||||||
|
grams: inputGrams.map(gram => gram.value).join(',')
|
||||||
|
}
|
||||||
|
textProcessor.inflect(data, response => setInputText(response.result));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleParse() {
|
||||||
|
const data: ITextRequest = {
|
||||||
|
text: inputText
|
||||||
|
}
|
||||||
|
textProcessor.parse(data, response => {
|
||||||
|
const grams = parseGrammemes(response.result);
|
||||||
|
setInputGrams(SelectorGrammems.filter(gram => grams.find(test => test === gram.value)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGenerateLexeme() {
|
||||||
|
if (forms.length > 0) {
|
||||||
|
if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data: ITextRequest = {
|
||||||
|
text: inputText
|
||||||
|
}
|
||||||
|
textProcessor.generateLexeme(data, response => {
|
||||||
|
const lexeme: IWordForm[] = [];
|
||||||
|
response.items.forEach(
|
||||||
|
form => {
|
||||||
|
const newForm: IWordForm = {
|
||||||
|
text: form.text,
|
||||||
|
grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram as Grammeme))
|
||||||
|
}
|
||||||
|
if (newForm.grams.length === 2 && !lexeme.some(test => wordFormEquals(test, newForm))) {
|
||||||
|
lexeme.push(newForm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setForms(lexeme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal canSubmit
|
||||||
|
title='Редактирование словоформ'
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
submitText='Сохранить'
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className='min-w-[40rem] max-w-[40rem] px-6'
|
||||||
|
>
|
||||||
|
<Overlay position='top-[-0.2rem] left-[7.5rem]'>
|
||||||
|
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<TextArea disabled spellCheck
|
||||||
|
label='Начальная форма'
|
||||||
|
placeholder='Термин в начальной форме'
|
||||||
|
rows={1}
|
||||||
|
value={term}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-3 mb-2'>
|
||||||
|
<Label text='Параметры словоформы' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-start justify-between w-full'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<TextArea
|
||||||
|
placeholder='Введите текст'
|
||||||
|
dimensions='min-w-[20rem] w-full min-h-[5rem]'
|
||||||
|
rows={2}
|
||||||
|
value={inputText}
|
||||||
|
onChange={event => setInputText(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div className='max-w-min'>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Генерировать словоформу'
|
||||||
|
icon={<ArrowLeftIcon size={5} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'} />}
|
||||||
|
disabled={textProcessor.loading || inputGrams.length == 0}
|
||||||
|
onClick={handleInflect}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Определить граммемы'
|
||||||
|
icon={<ArrowRightIcon
|
||||||
|
size={5}
|
||||||
|
color={!inputText ? 'text-disabled' : 'text-primary'}
|
||||||
|
/>}
|
||||||
|
disabled={textProcessor.loading || !inputText}
|
||||||
|
onClick={handleParse}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SelectGrammeme
|
||||||
|
placeholder='Выберите граммемы'
|
||||||
|
dimensions='min-w-[15rem] max-w-[15rem] h-full '
|
||||||
|
className='flex-grow'
|
||||||
|
value={inputGrams}
|
||||||
|
setValue={setInputGrams}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Overlay position='top-2 left-0'>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Внести словоформу'
|
||||||
|
icon={<CheckIcon
|
||||||
|
size={5}
|
||||||
|
color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}
|
||||||
|
/>}
|
||||||
|
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
|
||||||
|
onClick={handleAddForm}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Генерировать стандартные словоформы'
|
||||||
|
icon={<ChevronDoubleDownIcon
|
||||||
|
size={5}
|
||||||
|
color={!inputText ? 'text-disabled' : 'text-primary'}
|
||||||
|
/>}
|
||||||
|
disabled={textProcessor.loading || !inputText}
|
||||||
|
onClick={handleGenerateLexeme}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<h1 className='mt-3 mb-2 text-sm'>Заданные вручную словоформы [{forms.length}]</h1>
|
||||||
|
|
||||||
|
<div className='border overflow-y-auto max-h-[17.4rem] min-h-[17.4rem] mb-2'>
|
||||||
|
<WordFormsTable
|
||||||
|
forms={forms}
|
||||||
|
setForms={setForms}
|
||||||
|
onFormSelect={handleSelectForm}
|
||||||
|
loading={textProcessor.loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DlgEditWordForms;
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import MiniButton from '../../components/Common/MiniButton';
|
||||||
|
import Overlay from '../../components/Common/Overlay';
|
||||||
|
import DataTable, { createColumnHelper } from '../../components/DataTable';
|
||||||
|
import { CrossIcon } from '../../components/Icons';
|
||||||
|
import WordFormBadge from '../../components/Shared/WordFormBadge';
|
||||||
|
import { IWordForm } from '../../models/language';
|
||||||
|
|
||||||
|
interface WordFormsTableProps {
|
||||||
|
forms: IWordForm[]
|
||||||
|
setForms: React.Dispatch<React.SetStateAction<IWordForm[]>>
|
||||||
|
onFormSelect?: (form: IWordForm) => void
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<IWordForm>();
|
||||||
|
|
||||||
|
function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTableProps) {
|
||||||
|
const handleDeleteRow = useCallback(
|
||||||
|
(row: number) => {
|
||||||
|
setForms(
|
||||||
|
(prev) => {
|
||||||
|
const newForms: IWordForm[] = [];
|
||||||
|
prev.forEach(
|
||||||
|
(form, index) => {
|
||||||
|
if (index !== row) {
|
||||||
|
newForms.push(form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newForms;
|
||||||
|
});
|
||||||
|
}, [setForms]);
|
||||||
|
|
||||||
|
function handleResetAll() {
|
||||||
|
setForms([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor('text', {
|
||||||
|
id: 'text',
|
||||||
|
header: 'Текст',
|
||||||
|
size: 350,
|
||||||
|
minSize: 350,
|
||||||
|
maxSize: 350,
|
||||||
|
cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div>
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('grams', {
|
||||||
|
id: 'grams',
|
||||||
|
header: 'Граммемы',
|
||||||
|
size: 250,
|
||||||
|
minSize: 250,
|
||||||
|
maxSize: 250,
|
||||||
|
cell: props =>
|
||||||
|
<WordFormBadge
|
||||||
|
keyPrefix={props.cell.id}
|
||||||
|
form={props.row.original}
|
||||||
|
/>
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
size: 50,
|
||||||
|
minSize: 50,
|
||||||
|
maxSize: 50,
|
||||||
|
cell: props =>
|
||||||
|
<MiniButton noHover
|
||||||
|
tooltip='Удалить словоформу'
|
||||||
|
icon={<CrossIcon size={4} color='text-warning'/>}
|
||||||
|
onClick={() => handleDeleteRow(props.row.index)}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
], [handleDeleteRow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Overlay position='top-1 right-4'>
|
||||||
|
<MiniButton
|
||||||
|
tooltip='Сбросить все словоформы'
|
||||||
|
icon={<CrossIcon size={4} color={forms.length === 0 ? 'text-disabled' : 'text-warning'} />}
|
||||||
|
disabled={loading || forms.length === 0}
|
||||||
|
onClick={handleResetAll}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
<DataTable dense noFooter
|
||||||
|
data={forms}
|
||||||
|
columns={columns}
|
||||||
|
headPosition='0'
|
||||||
|
noDataComponent={
|
||||||
|
<span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
<p>Добавьте словоформу</p>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
onRowClicked={onFormSelect}
|
||||||
|
/>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WordFormsTable;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DlgEditWordForms';
|
|
@ -91,11 +91,6 @@ export function substituteTemplateArgs(expression: string, args: IArgumentValue[
|
||||||
.every(local => local.every(match => !(match in mapping)))
|
.every(local => local.every(match => !(match in mapping)))
|
||||||
).join(', ');
|
).join(', ');
|
||||||
|
|
||||||
console.log(body);
|
|
||||||
console.log(head);
|
|
||||||
console.log(args);
|
|
||||||
console.log(mapping);
|
|
||||||
|
|
||||||
if (!head) {
|
if (!head) {
|
||||||
return body;
|
return body;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useLayoutEffect } from 'react';
|
import { useCallback, useLayoutEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { MagnifyingGlassIcon } from '../../components/Icons';
|
import ConceptSearch from '../../components/Common/ConceptSearch';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useConceptNavigation } from '../../context/NagivationContext';
|
import { useConceptNavigation } from '../../context/NagivationContext';
|
||||||
import { ILibraryFilter } from '../../models/miscelanious';
|
import { ILibraryFilter } from '../../models/miscelanious';
|
||||||
|
@ -35,8 +35,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setStrategy,
|
||||||
const search = useLocation().search;
|
const search = useLocation().search;
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
|
function handleChangeQuery(newQuery: string) {
|
||||||
const newQuery = event.target.value;
|
|
||||||
setQuery(newQuery);
|
setQuery(newQuery);
|
||||||
setFilter(prev => ({
|
setFilter(prev => ({
|
||||||
query: newQuery,
|
query: newQuery,
|
||||||
|
@ -77,17 +76,11 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setStrategy,
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-center w-full gap-1'>
|
<div className='flex items-center justify-center w-full gap-1'>
|
||||||
<div className='relative min-w-[10rem] select-none'>
|
<ConceptSearch noBorder
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
|
|
||||||
<MagnifyingGlassIcon />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
placeholder='Поиск'
|
|
||||||
value={query}
|
value={query}
|
||||||
className='w-full p-2 pl-10 text-sm outline-none clr-input'
|
|
||||||
onChange={handleChangeQuery}
|
onChange={handleChangeQuery}
|
||||||
|
dimensions='min-w-[10rem] '
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<PickerStrategy
|
<PickerStrategy
|
||||||
value={strategy}
|
value={strategy}
|
||||||
onChange={handleChangeStrategy}
|
onChange={handleChangeStrategy}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import useWindowSize from '../../../hooks/useWindowSize';
|
||||||
import { CstType, IConstituenta, ICstCreateData, ICstRenameData } from '../../../models/rsform';
|
import { CstType, IConstituenta, ICstCreateData, ICstRenameData } from '../../../models/rsform';
|
||||||
import { SyntaxTree } from '../../../models/rslang';
|
import { SyntaxTree } from '../../../models/rslang';
|
||||||
import { globalIDs } from '../../../utils/constants';
|
import { globalIDs } from '../../../utils/constants';
|
||||||
|
import ViewConstituents from '../ViewConstituents';
|
||||||
import ConstituentaToolbar from './ConstituentaToolbar';
|
import ConstituentaToolbar from './ConstituentaToolbar';
|
||||||
import FormConstituenta from './FormConstituenta';
|
import FormConstituenta from './FormConstituenta';
|
||||||
import ViewSideConstituents from './ViewSideConstituents';
|
|
||||||
|
|
||||||
// Max height of content for left enditor pane.
|
// Max height of content for left enditor pane.
|
||||||
const UNFOLDED_HEIGHT = '59.1rem';
|
const UNFOLDED_HEIGHT = '59.1rem';
|
||||||
|
@ -117,11 +117,7 @@ function EditorConstituenta({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
<div tabIndex={-1}
|
|
||||||
className='max-w-[1500px]'
|
|
||||||
onKeyDown={handleInput}
|
|
||||||
>
|
|
||||||
<ConstituentaToolbar
|
<ConstituentaToolbar
|
||||||
isMutable={readyForEdit}
|
isMutable={readyForEdit}
|
||||||
isModified={isModified}
|
isModified={isModified}
|
||||||
|
@ -134,7 +130,10 @@ function EditorConstituenta({
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
onTemplates={() => onTemplates(activeID)}
|
onTemplates={() => onTemplates(activeID)}
|
||||||
/>
|
/>
|
||||||
<div className='flex justify-start'>
|
<div tabIndex={-1}
|
||||||
|
className='max-w-[1500px] flex justify-start w-full'
|
||||||
|
onKeyDown={handleInput}
|
||||||
|
>
|
||||||
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'>
|
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'>
|
||||||
<FormConstituenta id={globalIDs.constituenta_editor}
|
<FormConstituenta id={globalIDs.constituenta_editor}
|
||||||
constituenta={activeCst}
|
constituenta={activeCst}
|
||||||
|
@ -149,7 +148,8 @@ function EditorConstituenta({
|
||||||
</div>
|
</div>
|
||||||
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
|
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
|
||||||
<div className='w-full mt-[2.25rem] border h-fit'>
|
<div className='w-full mt-[2.25rem] border h-fit'>
|
||||||
<ViewSideConstituents
|
<ViewConstituents
|
||||||
|
schema={schema}
|
||||||
expression={activeCst?.definition_formal ?? ''}
|
expression={activeCst?.definition_formal ?? ''}
|
||||||
baseHeight={UNFOLDED_HEIGHT}
|
baseHeight={UNFOLDED_HEIGHT}
|
||||||
activeID={activeID}
|
activeID={activeID}
|
||||||
|
@ -157,7 +157,7 @@ function EditorConstituenta({
|
||||||
/>
|
/>
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditorConstituenta;
|
export default EditorConstituenta;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useRSForm } from '../../../context/RSFormContext';
|
||||||
import { IConstituenta, ICstRenameData, ICstUpdateData } from '../../../models/rsform';
|
import { IConstituenta, ICstRenameData, ICstUpdateData } from '../../../models/rsform';
|
||||||
import { SyntaxTree } from '../../../models/rslang';
|
import { SyntaxTree } from '../../../models/rslang';
|
||||||
import { labelCstTypification } from '../../../utils/labels';
|
import { labelCstTypification } from '../../../utils/labels';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import EditorRSExpression from '../EditorRSExpression';
|
||||||
|
|
||||||
interface FormConstituentaProps {
|
interface FormConstituentaProps {
|
||||||
id?: string
|
id?: string
|
||||||
|
|
|
@ -1,282 +0,0 @@
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import Dropdown from '../../../components/Common/Dropdown';
|
|
||||||
import DropdownButton from '../../../components/Common/DropdownButton';
|
|
||||||
import SelectorButton from '../../../components/Common/SelectorButton';
|
|
||||||
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '../../../components/DataTable';
|
|
||||||
import { CogIcon, FilterIcon, MagnifyingGlassIcon } from '../../../components/Icons';
|
|
||||||
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
|
|
||||||
import { useRSForm } from '../../../context/RSFormContext';
|
|
||||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
|
||||||
import useDropdown from '../../../hooks/useDropdown';
|
|
||||||
import useLocalStorage from '../../../hooks/useLocalStorage';
|
|
||||||
import useWindowSize from '../../../hooks/useWindowSize';
|
|
||||||
import { DependencyMode as CstSource } from '../../../models/miscelanious';
|
|
||||||
import { CstMatchMode } from '../../../models/miscelanious';
|
|
||||||
import { applyGraphFilter } from '../../../models/miscelaniousAPI';
|
|
||||||
import { CstType, IConstituenta } from '../../../models/rsform';
|
|
||||||
import { createMockConstituenta, isMockCst, matchConstituenta } from '../../../models/rsformAPI';
|
|
||||||
import { extractGlobals } from '../../../models/rslangAPI';
|
|
||||||
import { prefixes } from '../../../utils/constants';
|
|
||||||
import {
|
|
||||||
describeConstituenta, describeCstMathchMode,
|
|
||||||
describeCstSource, labelCstMathchMode,
|
|
||||||
labelCstSource
|
|
||||||
} from '../../../utils/labels';
|
|
||||||
|
|
||||||
// Height that should be left to accomodate navigation panel + bottom margin
|
|
||||||
const LOCAL_NAVIGATION_H = '2.1rem';
|
|
||||||
|
|
||||||
// Window width cutoff for expression show
|
|
||||||
const COLUMN_EXPRESSION_HIDE_THRESHOLD = 1500;
|
|
||||||
|
|
||||||
interface ViewSideConstituentsProps {
|
|
||||||
expression: string
|
|
||||||
baseHeight: string
|
|
||||||
activeID?: number
|
|
||||||
onOpenEdit: (cstID: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
|
||||||
|
|
||||||
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
|
||||||
const windowSize = useWindowSize();
|
|
||||||
const { noNavigation, colors } = useConceptTheme();
|
|
||||||
const { schema } = useRSForm();
|
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({'expression': true})
|
|
||||||
|
|
||||||
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
|
|
||||||
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
|
|
||||||
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', CstSource.ALL);
|
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
|
||||||
|
|
||||||
const matchModeMenu = useDropdown();
|
|
||||||
const sourceMenu = useDropdown();
|
|
||||||
|
|
||||||
useLayoutEffect(
|
|
||||||
() => {
|
|
||||||
setColumnVisibility(prev => {
|
|
||||||
const newValue = (windowSize.width ?? 0) >= COLUMN_EXPRESSION_HIDE_THRESHOLD;
|
|
||||||
if (newValue === prev['expression']) {
|
|
||||||
return prev;
|
|
||||||
} else {
|
|
||||||
return {'expression': newValue}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [windowSize]);
|
|
||||||
|
|
||||||
useLayoutEffect(
|
|
||||||
() => {
|
|
||||||
if (!schema?.items) {
|
|
||||||
setFilteredData([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let filtered: IConstituenta[] = [];
|
|
||||||
if (filterSource === CstSource.EXPRESSION) {
|
|
||||||
const aliases = extractGlobals(expression);
|
|
||||||
filtered = schema.items.filter((cst) => aliases.has(cst.alias));
|
|
||||||
const names = filtered.map(cst => cst.alias)
|
|
||||||
const diff = Array.from(aliases).filter(name => !names.includes(name));
|
|
||||||
if (diff.length > 0) {
|
|
||||||
diff.forEach(
|
|
||||||
(alias, index) => filtered.push(
|
|
||||||
createMockConstituenta(
|
|
||||||
schema.id,
|
|
||||||
-index,
|
|
||||||
alias,
|
|
||||||
CstType.BASE,
|
|
||||||
'Конституента отсутствует'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (!activeID) {
|
|
||||||
filtered = schema.items
|
|
||||||
} else {
|
|
||||||
filtered = applyGraphFilter(schema, activeID, filterSource);
|
|
||||||
}
|
|
||||||
if (filterText) {
|
|
||||||
filtered = filtered.filter(cst => matchConstituenta(cst, filterText, filterMatch));
|
|
||||||
}
|
|
||||||
setFilteredData(filtered);
|
|
||||||
}, [filterText, setFilteredData, filterSource, expression, schema?.items, schema, filterMatch, activeID]);
|
|
||||||
|
|
||||||
const handleRowClicked = useCallback(
|
|
||||||
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
|
||||||
if (event.altKey && !isMockCst(cst)) {
|
|
||||||
onOpenEdit(cst.id);
|
|
||||||
}
|
|
||||||
}, [onOpenEdit]);
|
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(
|
|
||||||
(cst: IConstituenta) => {
|
|
||||||
if (!isMockCst(cst)) {
|
|
||||||
onOpenEdit(cst.id);
|
|
||||||
}
|
|
||||||
}, [onOpenEdit]);
|
|
||||||
|
|
||||||
const handleMatchModeChange = useCallback(
|
|
||||||
(newValue: CstMatchMode) => {
|
|
||||||
matchModeMenu.hide();
|
|
||||||
setFilterMatch(newValue);
|
|
||||||
}, [matchModeMenu, setFilterMatch]);
|
|
||||||
|
|
||||||
const handleSourceChange = useCallback(
|
|
||||||
(newValue: CstSource) => {
|
|
||||||
sourceMenu.hide();
|
|
||||||
setFilterSource(newValue);
|
|
||||||
}, [sourceMenu, setFilterSource]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
columnHelper.accessor('alias', {
|
|
||||||
id: 'alias',
|
|
||||||
header: 'Имя',
|
|
||||||
size: 65,
|
|
||||||
minSize: 65,
|
|
||||||
footer: undefined,
|
|
||||||
cell: props =>
|
|
||||||
<ConstituentaBadge
|
|
||||||
theme={colors}
|
|
||||||
value={props.row.original}
|
|
||||||
prefixID={prefixes.cst_list}
|
|
||||||
/>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor(cst => describeConstituenta(cst), {
|
|
||||||
id: 'description',
|
|
||||||
header: 'Описание',
|
|
||||||
size: 1000,
|
|
||||||
minSize: 250,
|
|
||||||
maxSize: 1000,
|
|
||||||
cell: props =>
|
|
||||||
<div style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
|
|
||||||
}}>
|
|
||||||
{props.getValue()}
|
|
||||||
</div>
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('definition_formal', {
|
|
||||||
id: 'expression',
|
|
||||||
header: 'Выражение',
|
|
||||||
size: 2000,
|
|
||||||
minSize: 0,
|
|
||||||
maxSize: 2000,
|
|
||||||
enableHiding: true,
|
|
||||||
cell: props =>
|
|
||||||
<div style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
|
|
||||||
}}>
|
|
||||||
{props.getValue()}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
], [colors]);
|
|
||||||
|
|
||||||
const conditionalRowStyles = useMemo(
|
|
||||||
(): IConditionalStyle<IConstituenta>[] => [
|
|
||||||
{
|
|
||||||
when: (cst: IConstituenta) => cst.id === activeID,
|
|
||||||
style: {
|
|
||||||
backgroundColor: colors.bgSelected
|
|
||||||
},
|
|
||||||
}
|
|
||||||
], [activeID, colors]);
|
|
||||||
|
|
||||||
const maxHeight = useMemo(
|
|
||||||
() => {
|
|
||||||
const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
|
|
||||||
return (noNavigation ?
|
|
||||||
`calc(min(100vh - 8.2rem, ${siblingHeight}))`
|
|
||||||
: `calc(min(100vh - 11.7rem, ${siblingHeight}))`);
|
|
||||||
}, [noNavigation, baseHeight]);
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<div className='relative top-0 left-0 right-0 flex items-stretch justify-between gap-1 pl-2 border-b clr-input'>
|
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
|
|
||||||
<MagnifyingGlassIcon />
|
|
||||||
</div>
|
|
||||||
<input type='text'
|
|
||||||
className='w-full min-w-[6rem] pr-2 pl-8 py-1 outline-none select-none hover:text-clip clr-input'
|
|
||||||
placeholder='Поиск'
|
|
||||||
value={filterText}
|
|
||||||
onChange={event => setFilterText(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div className='flex'>
|
|
||||||
<div ref={matchModeMenu.ref}>
|
|
||||||
<SelectorButton transparent tabIndex={-1}
|
|
||||||
tooltip='Настройка атрибутов для фильтрации'
|
|
||||||
dimensions='w-fit h-full'
|
|
||||||
icon={<FilterIcon size={5} />}
|
|
||||||
text={labelCstMathchMode(filterMatch)}
|
|
||||||
onClick={matchModeMenu.toggle}
|
|
||||||
/>
|
|
||||||
{ matchModeMenu.isActive &&
|
|
||||||
<Dropdown stretchLeft>
|
|
||||||
{Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
|
|
||||||
(value, index) => {
|
|
||||||
const matchMode = value as CstMatchMode;
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
key={`${prefixes.cst_match_mode_list}${index}`}
|
|
||||||
onClick={() => handleMatchModeChange(matchMode)}
|
|
||||||
>
|
|
||||||
<p><span className='font-semibold'>{labelCstMathchMode(matchMode)}:</span> {describeCstMathchMode(matchMode)}</p>
|
|
||||||
</DropdownButton>);
|
|
||||||
})}
|
|
||||||
</Dropdown>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref={sourceMenu.ref}>
|
|
||||||
<SelectorButton transparent tabIndex={-1}
|
|
||||||
tooltip='Настройка фильтрации по графу термов'
|
|
||||||
dimensions='w-fit h-full'
|
|
||||||
icon={<CogIcon size={4} />}
|
|
||||||
text={labelCstSource(filterSource)}
|
|
||||||
onClick={sourceMenu.toggle}
|
|
||||||
/>
|
|
||||||
{sourceMenu.isActive ?
|
|
||||||
<Dropdown stretchLeft>
|
|
||||||
{Object.values(CstSource).filter(value => !isNaN(Number(value))).map(
|
|
||||||
(value, index) => {
|
|
||||||
const source = value as CstSource;
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
key={`${prefixes.cst_source_list}${index}`}
|
|
||||||
onClick={() => handleSourceChange(source)}
|
|
||||||
>
|
|
||||||
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p>
|
|
||||||
</DropdownButton>);
|
|
||||||
})}
|
|
||||||
</Dropdown> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}>
|
|
||||||
<DataTable dense noFooter
|
|
||||||
data={filteredData}
|
|
||||||
columns={columns}
|
|
||||||
conditionalRowStyles={conditionalRowStyles}
|
|
||||||
headPosition='0'
|
|
||||||
|
|
||||||
enableHiding
|
|
||||||
columnVisibility={columnVisibility}
|
|
||||||
onColumnVisibilityChange={setColumnVisibility}
|
|
||||||
|
|
||||||
noDataComponent={
|
|
||||||
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem] select-none'>
|
|
||||||
<p>Список конституент пуст</p>
|
|
||||||
<p>Измените параметры фильтра</p>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
onRowDoubleClicked={handleDoubleClick}
|
|
||||||
onRowClicked={handleRowClicked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ViewSideConstituents;
|
|
|
@ -2,8 +2,6 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import Button from '../../../components/Common/Button';
|
|
||||||
import { ConceptLoader } from '../../../components/Common/ConceptLoader';
|
|
||||||
import MiniButton from '../../../components/Common/MiniButton';
|
import MiniButton from '../../../components/Common/MiniButton';
|
||||||
import Overlay from '../../../components/Common/Overlay';
|
import Overlay from '../../../components/Common/Overlay';
|
||||||
import { ASTNetworkIcon } from '../../../components/Icons';
|
import { ASTNetworkIcon } from '../../../components/Icons';
|
||||||
|
@ -16,9 +14,8 @@ import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '../../../mode
|
||||||
import { TokenID } from '../../../models/rslang';
|
import { TokenID } from '../../../models/rslang';
|
||||||
import { labelTypification } from '../../../utils/labels';
|
import { labelTypification } from '../../../utils/labels';
|
||||||
import { getCstExpressionPrefix } from '../../../utils/misc';
|
import { getCstExpressionPrefix } from '../../../utils/misc';
|
||||||
import ParsingResult from './ParsingResult';
|
import RSAnalyzer from './RSAnalyzer';
|
||||||
import RSEditorControls from './RSEditControls';
|
import RSEditorControls from './RSEditControls';
|
||||||
import StatusBar from './StatusBar';
|
|
||||||
|
|
||||||
interface EditorRSExpressionProps {
|
interface EditorRSExpressionProps {
|
||||||
id?: string
|
id?: string
|
||||||
|
@ -126,6 +123,7 @@ function EditorRSExpression({
|
||||||
icon={<ASTNetworkIcon size={5} color='text-primary' />}
|
icon={<ASTNetworkIcon size={5} color='text-primary' />}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
<RSInput innerref={rsInput}
|
<RSInput innerref={rsInput}
|
||||||
value={value}
|
value={value}
|
||||||
minHeight='3.8rem'
|
minHeight='3.8rem'
|
||||||
|
@ -133,40 +131,20 @@ function EditorRSExpression({
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RSEditorControls
|
<RSEditorControls
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
/>
|
/>
|
||||||
<div className='w-full max-h-[4.5rem] min-h-[4.5rem] flex'>
|
|
||||||
<div className='flex flex-col text-sm'>
|
<RSAnalyzer
|
||||||
<Button noOutline
|
|
||||||
text='Проверить'
|
|
||||||
tooltip='Проверить формальное определение'
|
|
||||||
dimensions='w-[6.75rem] min-h-[3rem] z-pop rounded-none'
|
|
||||||
colors='clr-btn-default'
|
|
||||||
onClick={() => handleCheckExpression()}
|
|
||||||
/>
|
|
||||||
<StatusBar
|
|
||||||
isModified={isModified}
|
|
||||||
constituenta={activeCst}
|
|
||||||
parseData={parseData}
|
parseData={parseData}
|
||||||
/>
|
processing={loading}
|
||||||
</div>
|
isModified={isModified}
|
||||||
<div className='w-full overflow-y-auto text-sm border rounded-none'>
|
activeCst={activeCst}
|
||||||
{loading ? <ConceptLoader size={6} /> : null}
|
onCheckExpression={handleCheckExpression}
|
||||||
{(!loading && parseData) ?
|
|
||||||
<ParsingResult
|
|
||||||
data={parseData}
|
|
||||||
disabled={disabled}
|
|
||||||
onShowError={onShowError}
|
onShowError={onShowError}
|
||||||
/> : null}
|
/>
|
||||||
{(!loading && !parseData) ?
|
|
||||||
<input disabled
|
|
||||||
className='w-full px-2 py-1 text-base select-none h-fit clr-app'
|
|
||||||
placeholder='Результаты проверки выражения'
|
|
||||||
/> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import Button from '../../../components/Common/Button';
|
||||||
|
import { ConceptLoader } from '../../../components/Common/ConceptLoader';
|
||||||
|
import { IConstituenta } from '../../../models/rsform';
|
||||||
|
import { IExpressionParse, IRSErrorDescription } from '../../../models/rslang';
|
||||||
|
import ParsingResult from './ParsingResult';
|
||||||
|
import StatusBar from './StatusBar';
|
||||||
|
|
||||||
|
interface RSAnalyzerProps {
|
||||||
|
parseData?: IExpressionParse
|
||||||
|
processing?: boolean
|
||||||
|
|
||||||
|
activeCst?: IConstituenta
|
||||||
|
isModified: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
|
||||||
|
onCheckExpression: () => void
|
||||||
|
onShowError: (error: IRSErrorDescription) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function RSAnalyzer({
|
||||||
|
parseData, processing,
|
||||||
|
activeCst, disabled, isModified,
|
||||||
|
onCheckExpression, onShowError,
|
||||||
|
}: RSAnalyzerProps) {
|
||||||
|
return (
|
||||||
|
<div className='w-full max-h-[4.5rem] min-h-[4.5rem] flex'>
|
||||||
|
<div className='flex flex-col text-sm'>
|
||||||
|
<Button noOutline
|
||||||
|
text='Проверить'
|
||||||
|
tooltip='Проверить формальное определение'
|
||||||
|
dimensions='w-[6.75rem] min-h-[3rem] z-pop rounded-none'
|
||||||
|
colors='clr-btn-default'
|
||||||
|
onClick={() => onCheckExpression()}
|
||||||
|
/>
|
||||||
|
<StatusBar
|
||||||
|
isModified={isModified}
|
||||||
|
constituenta={activeCst}
|
||||||
|
parseData={parseData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='w-full overflow-y-auto text-sm border rounded-none'>
|
||||||
|
{processing ? <ConceptLoader size={6} /> : null}
|
||||||
|
{(!processing && parseData) ?
|
||||||
|
<ParsingResult
|
||||||
|
data={parseData}
|
||||||
|
disabled={disabled}
|
||||||
|
onShowError={onShowError}
|
||||||
|
/> : null}
|
||||||
|
{(!processing && !parseData) ?
|
||||||
|
<input disabled
|
||||||
|
className='w-full px-2 py-1 text-base select-none h-fit clr-app'
|
||||||
|
placeholder='Результаты проверки выражения'
|
||||||
|
/> : null}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RSAnalyzer;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './EditorRSExpression';
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { useCallback, useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import ConceptSearch from '../../../components/Common/ConceptSearch';
|
||||||
|
import Dropdown from '../../../components/Common/Dropdown';
|
||||||
|
import DropdownButton from '../../../components/Common/DropdownButton';
|
||||||
|
import SelectorButton from '../../../components/Common/SelectorButton';
|
||||||
|
import { CogIcon, FilterIcon } from '../../../components/Icons';
|
||||||
|
import useDropdown from '../../../hooks/useDropdown';
|
||||||
|
import useLocalStorage from '../../../hooks/useLocalStorage';
|
||||||
|
import { CstMatchMode, DependencyMode } from '../../../models/miscelanious';
|
||||||
|
import { applyGraphFilter } from '../../../models/miscelaniousAPI';
|
||||||
|
import { CstType, IConstituenta, IRSForm } from '../../../models/rsform';
|
||||||
|
import { createMockConstituenta, matchConstituenta } from '../../../models/rsformAPI';
|
||||||
|
import { extractGlobals } from '../../../models/rslangAPI';
|
||||||
|
import { prefixes } from '../../../utils/constants';
|
||||||
|
import { describeCstMathchMode, describeCstSource, labelCstMathchMode, labelCstSource } from '../../../utils/labels';
|
||||||
|
|
||||||
|
interface ConstituentsSearchProps {
|
||||||
|
schema?: IRSForm
|
||||||
|
activeID?: number
|
||||||
|
activeExpression: string
|
||||||
|
setFiltered: React.Dispatch<React.SetStateAction<IConstituenta[]>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConstituentsSearch({ schema, activeID, activeExpression, setFiltered }: ConstituentsSearchProps) {
|
||||||
|
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
|
||||||
|
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
|
||||||
|
const [filterSource, setFilterSource] = useLocalStorage('side-filter-dependency', DependencyMode.ALL);
|
||||||
|
|
||||||
|
const matchModeMenu = useDropdown();
|
||||||
|
const sourceMenu = useDropdown();
|
||||||
|
|
||||||
|
useLayoutEffect(
|
||||||
|
() => {
|
||||||
|
if (!schema || schema.items.length === 0) {
|
||||||
|
setFiltered([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let result: IConstituenta[] = [];
|
||||||
|
if (filterSource === DependencyMode.EXPRESSION) {
|
||||||
|
const aliases = extractGlobals(activeExpression);
|
||||||
|
console.log(aliases);
|
||||||
|
result = schema.items.filter((cst) => aliases.has(cst.alias));
|
||||||
|
const names = result.map(cst => cst.alias)
|
||||||
|
const diff = Array.from(aliases).filter(name => !names.includes(name));
|
||||||
|
if (diff.length > 0) {
|
||||||
|
diff.forEach(
|
||||||
|
(alias, index) => result.push(
|
||||||
|
createMockConstituenta(
|
||||||
|
-1,
|
||||||
|
-index,
|
||||||
|
alias,
|
||||||
|
CstType.BASE,
|
||||||
|
'Конституента отсутствует'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (!activeID) {
|
||||||
|
result = schema.items;
|
||||||
|
} else {
|
||||||
|
result = applyGraphFilter(schema, activeID, filterSource);
|
||||||
|
}
|
||||||
|
if (filterText) {
|
||||||
|
result = result.filter(cst => matchConstituenta(cst, filterText, filterMatch));
|
||||||
|
}
|
||||||
|
setFiltered(result);
|
||||||
|
}, [filterText, setFiltered, filterSource, activeExpression, schema?.items, schema, filterMatch, activeID]);
|
||||||
|
|
||||||
|
const handleMatchModeChange = useCallback(
|
||||||
|
(newValue: CstMatchMode) => {
|
||||||
|
matchModeMenu.hide();
|
||||||
|
setFilterMatch(newValue);
|
||||||
|
}, [matchModeMenu, setFilterMatch]);
|
||||||
|
|
||||||
|
const handleSourceChange = useCallback(
|
||||||
|
(newValue: DependencyMode) => {
|
||||||
|
sourceMenu.hide();
|
||||||
|
setFilterSource(newValue);
|
||||||
|
}, [sourceMenu, setFilterSource]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-stretch border-b clr-input'>
|
||||||
|
<ConceptSearch noBorder
|
||||||
|
value={filterText}
|
||||||
|
onChange={setFilterText}
|
||||||
|
dimensions='min-w-[6rem] pr-2 w-full'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div ref={matchModeMenu.ref}>
|
||||||
|
<SelectorButton transparent tabIndex={-1}
|
||||||
|
tooltip='Настройка атрибутов для фильтрации'
|
||||||
|
dimensions='w-fit h-full'
|
||||||
|
icon={<FilterIcon size={5} />}
|
||||||
|
text={labelCstMathchMode(filterMatch)}
|
||||||
|
onClick={matchModeMenu.toggle}
|
||||||
|
/>
|
||||||
|
{matchModeMenu.isActive ?
|
||||||
|
<Dropdown stretchLeft>
|
||||||
|
{Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
|
||||||
|
(value, index) => {
|
||||||
|
const matchMode = value as CstMatchMode;
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
key={`${prefixes.cst_match_mode_list}${index}`}
|
||||||
|
onClick={() => handleMatchModeChange(matchMode)}
|
||||||
|
>
|
||||||
|
<p><span className='font-semibold'>{labelCstMathchMode(matchMode)}:</span> {describeCstMathchMode(matchMode)}</p>
|
||||||
|
</DropdownButton>);
|
||||||
|
})}
|
||||||
|
</Dropdown> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={sourceMenu.ref}>
|
||||||
|
<SelectorButton transparent tabIndex={-1}
|
||||||
|
tooltip='Настройка фильтрации по графу термов'
|
||||||
|
dimensions='w-fit h-full pr-2'
|
||||||
|
icon={<CogIcon size={4} />}
|
||||||
|
text={labelCstSource(filterSource)}
|
||||||
|
onClick={sourceMenu.toggle}
|
||||||
|
/>
|
||||||
|
{sourceMenu.isActive ?
|
||||||
|
<Dropdown stretchLeft>
|
||||||
|
{Object.values(DependencyMode).filter(value => !isNaN(Number(value))).map(
|
||||||
|
(value, index) => {
|
||||||
|
const source = value as DependencyMode;
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
key={`${prefixes.cst_source_list}${index}`}
|
||||||
|
onClick={() => handleSourceChange(source)}
|
||||||
|
>
|
||||||
|
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p>
|
||||||
|
</DropdownButton>);
|
||||||
|
})}
|
||||||
|
</Dropdown> : null}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstituentsSearch;
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import DataTable, { createColumnHelper,IConditionalStyle, VisibilityState } from '../../../components/DataTable';
|
||||||
|
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
|
||||||
|
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||||
|
import useWindowSize from '../../../hooks/useWindowSize';
|
||||||
|
import { IConstituenta } from '../../../models/rsform';
|
||||||
|
import { isMockCst } from '../../../models/rsformAPI';
|
||||||
|
import { prefixes } from '../../../utils/constants';
|
||||||
|
import { describeConstituenta } from '../../../utils/labels';
|
||||||
|
|
||||||
|
interface ConstituentsTableProps {
|
||||||
|
items: IConstituenta[]
|
||||||
|
activeID?: number
|
||||||
|
onOpenEdit: (cstID: number) => void
|
||||||
|
denseThreshold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
|
||||||
|
function ConstituentsTable({
|
||||||
|
items, activeID, onOpenEdit,
|
||||||
|
denseThreshold = 9999
|
||||||
|
}: ConstituentsTableProps) {
|
||||||
|
const { colors } = useConceptTheme();
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({'expression': true});
|
||||||
|
|
||||||
|
useLayoutEffect(
|
||||||
|
() => {
|
||||||
|
setColumnVisibility(prev => {
|
||||||
|
const newValue = (windowSize.width ?? 0) >= denseThreshold;
|
||||||
|
if (newValue === prev['expression']) {
|
||||||
|
return prev;
|
||||||
|
} else {
|
||||||
|
return {'expression': newValue}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [windowSize, denseThreshold]);
|
||||||
|
|
||||||
|
const handleRowClicked = useCallback(
|
||||||
|
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||||
|
if (event.altKey && !isMockCst(cst)) {
|
||||||
|
onOpenEdit(cst.id);
|
||||||
|
}
|
||||||
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
|
const handleDoubleClick = useCallback(
|
||||||
|
(cst: IConstituenta) => {
|
||||||
|
if (!isMockCst(cst)) {
|
||||||
|
onOpenEdit(cst.id);
|
||||||
|
}
|
||||||
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor('alias', {
|
||||||
|
id: 'alias',
|
||||||
|
header: 'Имя',
|
||||||
|
size: 65,
|
||||||
|
minSize: 65,
|
||||||
|
footer: undefined,
|
||||||
|
cell: props =>
|
||||||
|
<ConstituentaBadge
|
||||||
|
theme={colors}
|
||||||
|
value={props.row.original}
|
||||||
|
prefixID={prefixes.cst_list}
|
||||||
|
/>
|
||||||
|
}),
|
||||||
|
columnHelper.accessor(cst => describeConstituenta(cst), {
|
||||||
|
id: 'description',
|
||||||
|
header: 'Описание',
|
||||||
|
size: 1000,
|
||||||
|
minSize: 250,
|
||||||
|
maxSize: 1000,
|
||||||
|
cell: props =>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
|
||||||
|
}}>
|
||||||
|
{props.getValue()}
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('definition_formal', {
|
||||||
|
id: 'expression',
|
||||||
|
header: 'Выражение',
|
||||||
|
size: 2000,
|
||||||
|
minSize: 0,
|
||||||
|
maxSize: 2000,
|
||||||
|
enableHiding: true,
|
||||||
|
cell: props =>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: isMockCst(props.row.original) ? colors.fgWarning : undefined
|
||||||
|
}}>
|
||||||
|
{props.getValue()}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
], [colors]);
|
||||||
|
|
||||||
|
const conditionalRowStyles = useMemo(
|
||||||
|
(): IConditionalStyle<IConstituenta>[] => [
|
||||||
|
{
|
||||||
|
when: (cst: IConstituenta) => cst.id === activeID,
|
||||||
|
style: {
|
||||||
|
backgroundColor: colors.bgSelected
|
||||||
|
},
|
||||||
|
}
|
||||||
|
], [activeID, colors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable dense noFooter
|
||||||
|
data={items}
|
||||||
|
columns={columns}
|
||||||
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
|
headPosition='0'
|
||||||
|
|
||||||
|
enableHiding
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
|
onColumnVisibilityChange={setColumnVisibility}
|
||||||
|
|
||||||
|
noDataComponent={
|
||||||
|
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem] select-none'>
|
||||||
|
<p>Список конституент пуст</p>
|
||||||
|
<p>Измените параметры фильтра</p>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
onRowDoubleClicked={handleDoubleClick}
|
||||||
|
onRowClicked={handleRowClicked}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstituentsTable;
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||||
|
import { IConstituenta, IRSForm } from '../../../models/rsform';
|
||||||
|
import ConstituentsSearch from './ConstituentsSearch';
|
||||||
|
import ConstituentsTable from './ConstituentsTable';
|
||||||
|
|
||||||
|
// Height that should be left to accomodate navigation panel + bottom margin
|
||||||
|
const LOCAL_NAVIGATION_H = '2.1rem';
|
||||||
|
|
||||||
|
// Window width cutoff for expression show
|
||||||
|
const COLUMN_EXPRESSION_HIDE_THRESHOLD = 1500;
|
||||||
|
|
||||||
|
interface ViewConstituentsProps {
|
||||||
|
expression: string
|
||||||
|
baseHeight: string
|
||||||
|
activeID?: number
|
||||||
|
schema?: IRSForm
|
||||||
|
onOpenEdit: (cstID: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewConstituents({ expression, baseHeight, schema, activeID, onOpenEdit }: ViewConstituentsProps) {
|
||||||
|
const { noNavigation } = useConceptTheme();
|
||||||
|
|
||||||
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||||
|
|
||||||
|
const maxHeight = useMemo(
|
||||||
|
() => {
|
||||||
|
const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
|
||||||
|
return (noNavigation ?
|
||||||
|
`calc(min(100vh - 8.2rem, ${siblingHeight}))`
|
||||||
|
: `calc(min(100vh - 11.7rem, ${siblingHeight}))`);
|
||||||
|
}, [noNavigation, baseHeight]);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<ConstituentsSearch
|
||||||
|
schema={schema}
|
||||||
|
activeID={activeID}
|
||||||
|
activeExpression={expression}
|
||||||
|
setFiltered={setFilteredData}
|
||||||
|
/>
|
||||||
|
<div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}>
|
||||||
|
<ConstituentsTable
|
||||||
|
items={filteredData}
|
||||||
|
activeID={activeID}
|
||||||
|
onOpenEdit={onOpenEdit}
|
||||||
|
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewConstituents;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './ViewConstituents';
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Module: CodeMirror customizations.
|
* Module: CodeMirror customizations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { syntaxTree } from '@codemirror/language';
|
import { syntaxTree } from '@codemirror/language';
|
||||||
import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
||||||
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||||
|
@ -200,7 +199,6 @@ export function domTooltipEntityReference(ref: IEntityReference, cst: IConstitue
|
||||||
grams.appendChild(gram);
|
grams.appendChild(gram);
|
||||||
});
|
});
|
||||||
dom.appendChild(grams);
|
dom.appendChild(grams);
|
||||||
|
|
||||||
return { dom: dom };
|
return { dom: dom };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user